mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-13 17:18:08 -05:00
Compare commits
17 Commits
add-llm-ma
...
fix/execut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fc2e3bbd8 | ||
|
|
3b6dc48033 | ||
|
|
3cab0c1240 | ||
|
|
2416975c30 | ||
|
|
bb8aab7bd4 | ||
|
|
a04b891e1c | ||
|
|
a304332bea | ||
|
|
01cfac9d5a | ||
|
|
f482eb668b | ||
|
|
fc8434fb30 | ||
|
|
3ae08cd48e | ||
|
|
4db13837b9 | ||
|
|
df87867625 | ||
|
|
e503126170 | ||
|
|
7ee28197a3 | ||
|
|
818de26d24 | ||
|
|
4a7bc006a8 |
@@ -28,6 +28,7 @@ from backend.executor.manager import get_db_async_client
|
||||
from backend.util.settings import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class ExecutionAnalyticsRequest(BaseModel):
|
||||
@@ -63,6 +64,8 @@ class ExecutionAnalyticsResult(BaseModel):
|
||||
score: Optional[float]
|
||||
status: str # "success", "failed", "skipped"
|
||||
error_message: Optional[str] = None
|
||||
started_at: Optional[datetime] = None
|
||||
ended_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class ExecutionAnalyticsResponse(BaseModel):
|
||||
@@ -224,11 +227,6 @@ async def generate_execution_analytics(
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate model configuration
|
||||
settings = Settings()
|
||||
if not settings.secrets.openai_internal_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API key not configured")
|
||||
|
||||
# Get database client
|
||||
db_client = get_db_async_client()
|
||||
|
||||
@@ -320,6 +318,8 @@ async def generate_execution_analytics(
|
||||
),
|
||||
status="skipped",
|
||||
error_message=None, # Not an error - just already processed
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -349,6 +349,9 @@ async def _process_batch(
|
||||
) -> list[ExecutionAnalyticsResult]:
|
||||
"""Process a batch of executions concurrently."""
|
||||
|
||||
if not settings.secrets.openai_internal_api_key:
|
||||
raise HTTPException(status_code=500, detail="OpenAI API key not configured")
|
||||
|
||||
async def process_single_execution(execution) -> ExecutionAnalyticsResult:
|
||||
try:
|
||||
# Generate activity status and score using the specified model
|
||||
@@ -387,6 +390,8 @@ async def _process_batch(
|
||||
score=None,
|
||||
status="skipped",
|
||||
error_message="Activity generation returned None",
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
|
||||
# Update the execution stats
|
||||
@@ -416,6 +421,8 @@ async def _process_batch(
|
||||
summary_text=activity_response["activity_status"],
|
||||
score=activity_response["correctness_score"],
|
||||
status="success",
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -429,6 +436,8 @@ async def _process_batch(
|
||||
score=None,
|
||||
status="failed",
|
||||
error_message=str(e),
|
||||
started_at=execution.started_at,
|
||||
ended_at=execution.ended_at,
|
||||
)
|
||||
|
||||
# Process all executions in the batch concurrently
|
||||
|
||||
@@ -495,8 +495,14 @@ class SmartDecisionMakerBlock(Block):
|
||||
}
|
||||
|
||||
properties = {}
|
||||
field_mapping = {}
|
||||
|
||||
for link in links:
|
||||
field_name = link.sink_name
|
||||
|
||||
clean_field_name = SmartDecisionMakerBlock.cleanup(field_name)
|
||||
field_mapping[clean_field_name] = field_name
|
||||
|
||||
sink_block_input_schema = sink_node.input_default["input_schema"]
|
||||
sink_block_properties = sink_block_input_schema.get("properties", {}).get(
|
||||
link.sink_name, {}
|
||||
@@ -506,7 +512,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
if "description" in sink_block_properties
|
||||
else f"The {link.sink_name} of the tool"
|
||||
)
|
||||
properties[link.sink_name] = {
|
||||
properties[clean_field_name] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
"default": json.dumps(sink_block_properties.get("default", None)),
|
||||
@@ -519,7 +525,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
# Store node info for later use in output processing
|
||||
tool_function["_field_mapping"] = field_mapping
|
||||
tool_function["_sink_node_id"] = sink_node.id
|
||||
|
||||
return {"type": "function", "function": tool_function}
|
||||
@@ -1129,8 +1135,9 @@ class SmartDecisionMakerBlock(Block):
|
||||
original_field_name = field_mapping.get(clean_arg_name, clean_arg_name)
|
||||
arg_value = tool_args.get(clean_arg_name)
|
||||
|
||||
sanitized_arg_name = self.cleanup(original_field_name)
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{sanitized_arg_name}"
|
||||
# Use original_field_name directly (not sanitized) to match link sink_name
|
||||
# The field_mapping already translates from LLM's cleaned names to original names
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{original_field_name}"
|
||||
|
||||
logger.debug(
|
||||
"[SmartDecisionMakerBlock|geid:%s|neid:%s] emit %s",
|
||||
|
||||
@@ -196,6 +196,15 @@ class TestXMLParserBlockSecurity:
|
||||
async for _ in block.run(XMLParserBlock.Input(input_xml=large_xml)):
|
||||
pass
|
||||
|
||||
async def test_rejects_text_outside_root(self):
|
||||
"""Ensure parser surfaces readable errors for invalid root text."""
|
||||
block = XMLParserBlock()
|
||||
invalid_xml = "<root><child>value</child></root> trailing"
|
||||
|
||||
with pytest.raises(ValueError, match="text outside the root element"):
|
||||
async for _ in block.run(XMLParserBlock.Input(input_xml=invalid_xml)):
|
||||
pass
|
||||
|
||||
|
||||
class TestStoreMediaFileSecurity:
|
||||
"""Test file storage security limits."""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from gravitasml.parser import Parser
|
||||
from gravitasml.token import tokenize
|
||||
from gravitasml.token import Token, tokenize
|
||||
|
||||
from backend.data.block import Block, BlockOutput, BlockSchemaInput, BlockSchemaOutput
|
||||
from backend.data.model import SchemaField
|
||||
@@ -25,6 +25,38 @@ class XMLParserBlock(Block):
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_tokens(tokens: list[Token]) -> None:
|
||||
"""Ensure the XML has a single root element and no stray text."""
|
||||
if not tokens:
|
||||
raise ValueError("XML input is empty.")
|
||||
|
||||
depth = 0
|
||||
root_seen = False
|
||||
|
||||
for token in tokens:
|
||||
if token.type == "TAG_OPEN":
|
||||
if depth == 0 and root_seen:
|
||||
raise ValueError("XML must have a single root element.")
|
||||
depth += 1
|
||||
if depth == 1:
|
||||
root_seen = True
|
||||
elif token.type == "TAG_CLOSE":
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
raise SyntaxError("Unexpected closing tag in XML input.")
|
||||
elif token.type in {"TEXT", "ESCAPE"}:
|
||||
if depth == 0 and token.value:
|
||||
raise ValueError(
|
||||
"XML contains text outside the root element; "
|
||||
"wrap content in a single root tag."
|
||||
)
|
||||
|
||||
if depth != 0:
|
||||
raise SyntaxError("Unclosed tag detected in XML input.")
|
||||
if not root_seen:
|
||||
raise ValueError("XML must include a root element.")
|
||||
|
||||
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
# Security fix: Add size limits to prevent XML bomb attacks
|
||||
MAX_XML_SIZE = 10 * 1024 * 1024 # 10MB limit for XML input
|
||||
@@ -35,7 +67,9 @@ class XMLParserBlock(Block):
|
||||
)
|
||||
|
||||
try:
|
||||
tokens = tokenize(input_data.input_xml)
|
||||
tokens = list(tokenize(input_data.input_xml))
|
||||
self._validate_tokens(tokens)
|
||||
|
||||
parser = Parser(tokens)
|
||||
parsed_result = parser.parse()
|
||||
yield "parsed_xml", parsed_result
|
||||
|
||||
@@ -104,7 +104,7 @@ async def get_accuracy_trends_and_alerts(
|
||||
AND e."executionStatus" IN ('COMPLETED', 'FAILED', 'TERMINATED')
|
||||
{user_filter}
|
||||
GROUP BY DATE(e."createdAt")
|
||||
HAVING COUNT(*) >= 3 -- Need at least 3 executions per day
|
||||
HAVING COUNT(*) >= 1 -- Include all days with at least 1 execution
|
||||
),
|
||||
trends AS (
|
||||
SELECT
|
||||
|
||||
@@ -153,8 +153,14 @@ class GraphExecutionMeta(BaseDbModel):
|
||||
nodes_input_masks: Optional[dict[str, BlockInput]]
|
||||
preset_id: Optional[str]
|
||||
status: ExecutionStatus
|
||||
started_at: datetime
|
||||
ended_at: datetime
|
||||
started_at: Optional[datetime] = Field(
|
||||
None,
|
||||
description="When execution started running. Null if not yet started (QUEUED).",
|
||||
)
|
||||
ended_at: Optional[datetime] = Field(
|
||||
None,
|
||||
description="When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW).",
|
||||
)
|
||||
is_shared: bool = False
|
||||
share_token: Optional[str] = None
|
||||
|
||||
@@ -229,10 +235,8 @@ class GraphExecutionMeta(BaseDbModel):
|
||||
|
||||
@staticmethod
|
||||
def from_db(_graph_exec: AgentGraphExecution):
|
||||
now = datetime.now(timezone.utc)
|
||||
# TODO: make started_at and ended_at optional
|
||||
start_time = _graph_exec.startedAt or _graph_exec.createdAt
|
||||
end_time = _graph_exec.updatedAt or now
|
||||
start_time = _graph_exec.startedAt
|
||||
end_time = _graph_exec.endedAt
|
||||
|
||||
try:
|
||||
stats = GraphExecutionStats.model_validate(_graph_exec.stats)
|
||||
@@ -900,6 +904,14 @@ async def update_graph_execution_stats(
|
||||
|
||||
if status:
|
||||
update_data["executionStatus"] = status
|
||||
# Set endedAt when execution reaches a terminal status
|
||||
terminal_statuses = [
|
||||
ExecutionStatus.COMPLETED,
|
||||
ExecutionStatus.FAILED,
|
||||
ExecutionStatus.TERMINATED,
|
||||
]
|
||||
if status in terminal_statuses:
|
||||
update_data["endedAt"] = datetime.now(tz=timezone.utc)
|
||||
|
||||
where_clause: AgentGraphExecutionWhereInput = {"id": graph_exec_id}
|
||||
|
||||
|
||||
@@ -60,8 +60,10 @@ class LateExecutionMonitor:
|
||||
if not all_late_executions:
|
||||
return "No late executions detected."
|
||||
|
||||
# Sort by created time (oldest first)
|
||||
all_late_executions.sort(key=lambda x: x.started_at)
|
||||
# Sort by started time (oldest first), with None values (unstarted) first
|
||||
all_late_executions.sort(
|
||||
key=lambda x: x.started_at or datetime.min.replace(tzinfo=timezone.utc)
|
||||
)
|
||||
|
||||
num_total_late = len(all_late_executions)
|
||||
num_queued = len(queued_late_executions)
|
||||
@@ -74,7 +76,7 @@ class LateExecutionMonitor:
|
||||
was_truncated = num_total_late > tuncate_size
|
||||
|
||||
late_execution_details = [
|
||||
f"* `Execution ID: {exec.id}, Graph ID: {exec.graph_id}v{exec.graph_version}, User ID: {exec.user_id}, Status: {exec.status}, Created At: {exec.started_at.isoformat()}`"
|
||||
f"* `Execution ID: {exec.id}, Graph ID: {exec.graph_id}v{exec.graph_version}, User ID: {exec.user_id}, Status: {exec.status}, Started At: {exec.started_at.isoformat() if exec.started_at else 'Not started'}`"
|
||||
for exec in truncated_executions
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentGraphExecution" ADD COLUMN "endedAt" TIMESTAMP(3);
|
||||
|
||||
-- Set endedAt to updatedAt for existing records with terminal status only
|
||||
UPDATE "AgentGraphExecution"
|
||||
SET "endedAt" = "updatedAt"
|
||||
WHERE "endedAt" IS NULL
|
||||
AND "executionStatus" IN ('COMPLETED', 'FAILED', 'TERMINATED');
|
||||
8
autogpt_platform/backend/poetry.lock
generated
8
autogpt_platform/backend/poetry.lock
generated
@@ -1924,14 +1924,14 @@ google = ["google-api-python-client (>=2.0.0)", "google-auth (>=2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "gravitasml"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "gravitasml-0.1.3-py3-none-any.whl", hash = "sha256:51ff98b4564b7a61f7796f18d5f2558b919d30b3722579296089645b7bc18b85"},
|
||||
{file = "gravitasml-0.1.3.tar.gz", hash = "sha256:04d240b9fa35878252d57a36032130b6516487468847fcdced1022c032a20f57"},
|
||||
{file = "gravitasml-0.1.4-py3-none-any.whl", hash = "sha256:671a18b11d3d8a0e270c6a80c72cd058458b18d5ef7560d00010e962ab1bca74"},
|
||||
{file = "gravitasml-0.1.4.tar.gz", hash = "sha256:35d0d9fec7431817482d53d9c976e375557c3e041d1eb6928e809324a8c866e3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -7295,4 +7295,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "b762806d5d58fcf811220890c4705a16dc62b33387af43e3a29399c62a641098"
|
||||
content-hash = "a93ba0cea3b465cb6ec3e3f258b383b09f84ea352ccfdbfa112902cde5653fc6"
|
||||
|
||||
@@ -27,7 +27,7 @@ google-api-python-client = "^2.177.0"
|
||||
google-auth-oauthlib = "^1.2.2"
|
||||
google-cloud-storage = "^3.2.0"
|
||||
googlemaps = "^4.10.0"
|
||||
gravitasml = "^0.1.3"
|
||||
gravitasml = "^0.1.4"
|
||||
groq = "^0.30.0"
|
||||
html2text = "^2024.2.26"
|
||||
jinja2 = "^3.1.6"
|
||||
|
||||
@@ -383,6 +383,7 @@ model AgentGraphExecution {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime? @updatedAt
|
||||
startedAt DateTime?
|
||||
endedAt DateTime?
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
|
||||
|
||||
146
autogpt_platform/cloudflare_worker.js
Normal file
146
autogpt_platform/cloudflare_worker.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Cloudflare Workers Script for docs.agpt.co → agpt.co/docs migration
|
||||
*
|
||||
* Deploy this script to handle all redirects with a single JavaScript file.
|
||||
* No rule limits, easy to maintain, handles all edge cases.
|
||||
*/
|
||||
|
||||
// URL mapping for special cases that don't follow patterns
|
||||
const SPECIAL_MAPPINGS = {
|
||||
// Root page
|
||||
'/': '/docs/platform',
|
||||
|
||||
// Special cases that don't follow standard patterns
|
||||
'/platform/d_id/': '/docs/integrations/block-integrations/d-id',
|
||||
'/platform/blocks/blocks/': '/docs/integrations',
|
||||
'/platform/blocks/decoder_block/': '/docs/integrations/block-integrations/text-decoder',
|
||||
'/platform/blocks/http': '/docs/integrations/block-integrations/send-web-request',
|
||||
'/platform/blocks/llm/': '/docs/integrations/block-integrations/ai-and-llm',
|
||||
'/platform/blocks/time_blocks': '/docs/integrations/block-integrations/time-and-date',
|
||||
'/platform/blocks/text_to_speech_block': '/docs/integrations/block-integrations/text-to-speech',
|
||||
'/platform/blocks/ai_shortform_video_block': '/docs/integrations/block-integrations/ai-shortform-video',
|
||||
'/platform/blocks/replicate_flux_advanced': '/docs/integrations/block-integrations/replicate-flux-advanced',
|
||||
'/platform/blocks/flux_kontext': '/docs/integrations/block-integrations/flux-kontext',
|
||||
'/platform/blocks/ai_condition/': '/docs/integrations/block-integrations/ai-condition',
|
||||
'/platform/blocks/email_block': '/docs/integrations/block-integrations/email',
|
||||
'/platform/blocks/google_maps': '/docs/integrations/block-integrations/google-maps',
|
||||
'/platform/blocks/google/gmail': '/docs/integrations/block-integrations/gmail',
|
||||
'/platform/blocks/github/issues/': '/docs/integrations/block-integrations/github-issues',
|
||||
'/platform/blocks/github/repo/': '/docs/integrations/block-integrations/github-repo',
|
||||
'/platform/blocks/github/pull_requests': '/docs/integrations/block-integrations/github-pull-requests',
|
||||
'/platform/blocks/twitter/twitter': '/docs/integrations/block-integrations/twitter',
|
||||
'/classic/setup/': '/docs/classic/setup/setting-up-autogpt-classic',
|
||||
'/code-of-conduct/': '/docs/classic/help-us-improve-autogpt/code-of-conduct',
|
||||
'/contributing/': '/docs/classic/contributing',
|
||||
'/contribute/': '/docs/contribute',
|
||||
'/forge/components/introduction/': '/docs/classic/forge/introduction'
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform path by replacing underscores with hyphens and removing trailing slashes
|
||||
*/
|
||||
function transformPath(path) {
|
||||
return path.replace(/_/g, '-').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle docs.agpt.co redirects
|
||||
*/
|
||||
function handleDocsRedirect(url) {
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Check special mappings first
|
||||
if (SPECIAL_MAPPINGS[pathname]) {
|
||||
return `https://agpt.co${SPECIAL_MAPPINGS[pathname]}`;
|
||||
}
|
||||
|
||||
// Pattern-based redirects
|
||||
|
||||
// Platform blocks: /platform/blocks/* → /docs/integrations/block-integrations/*
|
||||
if (pathname.startsWith('/platform/blocks/')) {
|
||||
const blockName = pathname.substring('/platform/blocks/'.length);
|
||||
const transformedName = transformPath(blockName);
|
||||
return `https://agpt.co/docs/integrations/block-integrations/${transformedName}`;
|
||||
}
|
||||
|
||||
// Platform contributing: /platform/contributing/* → /docs/platform/contributing/*
|
||||
if (pathname.startsWith('/platform/contributing/')) {
|
||||
const subPath = pathname.substring('/platform/contributing/'.length);
|
||||
return `https://agpt.co/docs/platform/contributing/${subPath}`;
|
||||
}
|
||||
|
||||
// Platform general: /platform/* → /docs/platform/* (with underscore→hyphen)
|
||||
if (pathname.startsWith('/platform/')) {
|
||||
const subPath = pathname.substring('/platform/'.length);
|
||||
const transformedPath = transformPath(subPath);
|
||||
return `https://agpt.co/docs/platform/${transformedPath}`;
|
||||
}
|
||||
|
||||
// Forge components: /forge/components/* → /docs/classic/forge/introduction/*
|
||||
if (pathname.startsWith('/forge/components/')) {
|
||||
const subPath = pathname.substring('/forge/components/'.length);
|
||||
return `https://agpt.co/docs/classic/forge/introduction/${subPath}`;
|
||||
}
|
||||
|
||||
// Forge general: /forge/* → /docs/classic/forge/*
|
||||
if (pathname.startsWith('/forge/')) {
|
||||
const subPath = pathname.substring('/forge/'.length);
|
||||
return `https://agpt.co/docs/classic/forge/${subPath}`;
|
||||
}
|
||||
|
||||
// Classic: /classic/* → /docs/classic/*
|
||||
if (pathname.startsWith('/classic/')) {
|
||||
const subPath = pathname.substring('/classic/'.length);
|
||||
return `https://agpt.co/docs/classic/${subPath}`;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'https://agpt.co/docs/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Worker function
|
||||
*/
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Only handle docs.agpt.co requests
|
||||
if (url.hostname === 'docs.agpt.co') {
|
||||
const redirectUrl = handleDocsRedirect(url);
|
||||
|
||||
return new Response(null, {
|
||||
status: 301,
|
||||
headers: {
|
||||
'Location': redirectUrl,
|
||||
'Cache-Control': 'max-age=300' // Cache redirects for 5 minutes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For non-docs requests, pass through or return 404
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
};
|
||||
|
||||
// Test function for local development
|
||||
export function testRedirects() {
|
||||
const testCases = [
|
||||
'https://docs.agpt.co/',
|
||||
'https://docs.agpt.co/platform/getting-started/',
|
||||
'https://docs.agpt.co/platform/advanced_setup/',
|
||||
'https://docs.agpt.co/platform/blocks/basic/',
|
||||
'https://docs.agpt.co/platform/blocks/ai_condition/',
|
||||
'https://docs.agpt.co/classic/setup/',
|
||||
'https://docs.agpt.co/forge/components/agents/',
|
||||
'https://docs.agpt.co/contributing/',
|
||||
'https://docs.agpt.co/unknown-page'
|
||||
];
|
||||
|
||||
console.log('Testing redirects:');
|
||||
testCases.forEach(testUrl => {
|
||||
const url = new URL(testUrl);
|
||||
const result = handleDocsRedirect(url);
|
||||
console.log(`${testUrl} → ${result}`);
|
||||
});
|
||||
}
|
||||
@@ -46,14 +46,15 @@
|
||||
"@radix-ui/react-scroll-area": "1.2.10",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slider": "1.3.6",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@rjsf/core": "5.24.13",
|
||||
"@rjsf/utils": "5.24.13",
|
||||
"@rjsf/validator-ajv8": "5.24.13",
|
||||
"@rjsf/core": "6.1.2",
|
||||
"@rjsf/utils": "6.1.2",
|
||||
"@rjsf/validator-ajv8": "6.1.2",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@supabase/ssr": "0.7.0",
|
||||
"@supabase/supabase-js": "2.78.0",
|
||||
|
||||
3804
autogpt_platform/frontend/pnpm-lock.yaml
generated
3804
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,8 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
"Execution ID",
|
||||
"Status",
|
||||
"Score",
|
||||
"Started At",
|
||||
"Ended At",
|
||||
"Summary Text",
|
||||
"Error Message",
|
||||
];
|
||||
@@ -62,6 +64,8 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
result.exec_id,
|
||||
result.status,
|
||||
result.score?.toString() || "",
|
||||
result.started_at ? new Date(result.started_at).toLocaleString() : "",
|
||||
result.ended_at ? new Date(result.ended_at).toLocaleString() : "",
|
||||
`"${(result.summary_text || "").replace(/"/g, '""')}"`, // Escape quotes in summary
|
||||
`"${(result.error_message || "").replace(/"/g, '""')}"`, // Escape quotes in error
|
||||
]);
|
||||
@@ -248,15 +252,13 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{(result.summary_text || result.error_message) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => toggleRowExpansion(result.exec_id)}
|
||||
>
|
||||
<EyeIcon size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => toggleRowExpansion(result.exec_id)}
|
||||
>
|
||||
<EyeIcon size={16} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -264,6 +266,44 @@ export function AnalyticsResultsTable({ results }: Props) {
|
||||
<tr>
|
||||
<td colSpan={7} className="bg-gray-50 px-4 py-3">
|
||||
<div className="space-y-3">
|
||||
{/* Timestamps section */}
|
||||
<div className="grid grid-cols-2 gap-4 border-b border-gray-200 pb-3">
|
||||
<div>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-xs font-medium text-gray-600"
|
||||
>
|
||||
Started At:
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-sm text-gray-700"
|
||||
>
|
||||
{result.started_at
|
||||
? new Date(
|
||||
result.started_at,
|
||||
).toLocaleString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-xs font-medium text-gray-600"
|
||||
>
|
||||
Ended At:
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-sm text-gray-700"
|
||||
>
|
||||
{result.ended_at
|
||||
? new Date(result.ended_at).toLocaleString()
|
||||
: "—"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result.summary_text && (
|
||||
<div>
|
||||
<Text
|
||||
|
||||
@@ -541,7 +541,19 @@ export function ExecutionAnalyticsForm() {
|
||||
{/* Accuracy Trends Display */}
|
||||
{trendsData && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Execution Accuracy Trends</h3>
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-lg font-semibold">Execution Accuracy Trends</h3>
|
||||
<div className="rounded-md bg-blue-50 px-3 py-2 text-xs text-blue-700">
|
||||
<p className="font-medium">
|
||||
Chart Filters (matches monitoring system):
|
||||
</p>
|
||||
<ul className="mt-1 list-inside list-disc space-y-1">
|
||||
<li>Only days with ≥1 execution with correctness score</li>
|
||||
<li>Last 30 days</li>
|
||||
<li>Averages calculated from scored executions only</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert Section */}
|
||||
{trendsData.alert && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthPopupResultMessage } from "@/components/renderers/input-renderer/fields/CredentialField/models/OAuthCredentialModal/useOAuthCredentialModal";
|
||||
import { OAuthPopupResultMessage } from "./types";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// This route is intended to be used as the callback for integration OAuth flows,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export type OAuthPopupResultMessage = { message_type: "oauth_popup_result" } & (
|
||||
| {
|
||||
success: true;
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
);
|
||||
@@ -5,7 +5,7 @@ import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ClockIcon, PlayIcon } from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||
import { useRunInputDialog } from "./useRunInputDialog";
|
||||
import { CronSchedulerDialog } from "../CronSchedulerDialog/CronSchedulerDialog";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
|
||||
import { isCredentialFieldSchema } from "@/components/renderers/input-renderer/fields/CredentialField/helpers";
|
||||
import { isCredentialFieldSchema } from "@/components/renderers/InputRenderer/custom/CredentialField/helpers";
|
||||
|
||||
export const useRunInputDialog = ({
|
||||
setIsOpen,
|
||||
|
||||
@@ -12,16 +12,59 @@ import {
|
||||
import { useDraftRecoveryPopup } from "./useDraftRecoveryPopup";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { DraftDiff } from "@/lib/dexie/draft-utils";
|
||||
|
||||
interface DraftRecoveryPopupProps {
|
||||
isInitialLoadComplete: boolean;
|
||||
}
|
||||
|
||||
function formatDiffSummary(diff: DraftDiff | null): string {
|
||||
if (!diff) return "";
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
// Node changes
|
||||
const nodeChanges: string[] = [];
|
||||
if (diff.nodes.added > 0) nodeChanges.push(`+${diff.nodes.added}`);
|
||||
if (diff.nodes.removed > 0) nodeChanges.push(`-${diff.nodes.removed}`);
|
||||
if (diff.nodes.modified > 0) nodeChanges.push(`~${diff.nodes.modified}`);
|
||||
|
||||
if (nodeChanges.length > 0) {
|
||||
parts.push(
|
||||
`${nodeChanges.join("/")} block${diff.nodes.added + diff.nodes.removed + diff.nodes.modified !== 1 ? "s" : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Edge changes
|
||||
const edgeChanges: string[] = [];
|
||||
if (diff.edges.added > 0) edgeChanges.push(`+${diff.edges.added}`);
|
||||
if (diff.edges.removed > 0) edgeChanges.push(`-${diff.edges.removed}`);
|
||||
if (diff.edges.modified > 0) edgeChanges.push(`~${diff.edges.modified}`);
|
||||
|
||||
if (edgeChanges.length > 0) {
|
||||
parts.push(
|
||||
`${edgeChanges.join("/")} connection${diff.edges.added + diff.edges.removed + diff.edges.modified !== 1 ? "s" : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
export function DraftRecoveryPopup({
|
||||
isInitialLoadComplete,
|
||||
}: DraftRecoveryPopupProps) {
|
||||
const { isOpen, popupRef, nodeCount, edgeCount, savedAt, onLoad, onDiscard } =
|
||||
useDraftRecoveryPopup(isInitialLoadComplete);
|
||||
const {
|
||||
isOpen,
|
||||
popupRef,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
diff,
|
||||
savedAt,
|
||||
onLoad,
|
||||
onDiscard,
|
||||
} = useDraftRecoveryPopup(isInitialLoadComplete);
|
||||
|
||||
const diffSummary = formatDiffSummary(diff);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -72,10 +115,9 @@ export function DraftRecoveryPopup({
|
||||
variant="small"
|
||||
className="text-amber-700 dark:text-amber-400"
|
||||
>
|
||||
{nodeCount} block{nodeCount !== 1 ? "s" : ""}, {edgeCount}{" "}
|
||||
connection
|
||||
{edgeCount !== 1 ? "s" : ""} •{" "}
|
||||
{formatTimeAgo(new Date(savedAt).toISOString())}
|
||||
{diffSummary ||
|
||||
`${nodeCount} block${nodeCount !== 1 ? "s" : ""}, ${edgeCount} connection${edgeCount !== 1 ? "s" : ""}`}{" "}
|
||||
• {formatTimeAgo(new Date(savedAt).toISOString())}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useDraftRecoveryPopup = (isInitialLoadComplete: boolean) => {
|
||||
savedAt,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
diff,
|
||||
loadDraft: onLoad,
|
||||
discardDraft: onDiscard,
|
||||
} = useDraftManager(isInitialLoadComplete);
|
||||
@@ -54,6 +55,7 @@ export const useDraftRecoveryPopup = (isInitialLoadComplete: boolean) => {
|
||||
isOpen,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
diff,
|
||||
savedAt,
|
||||
onLoad,
|
||||
onDiscard,
|
||||
|
||||
@@ -48,8 +48,6 @@ export const resolveCollisions: CollisionAlgorithm = (
|
||||
const width = (node.width ?? node.measured?.width ?? 0) + margin * 2;
|
||||
const height = (node.height ?? node.measured?.height ?? 0) + margin * 2;
|
||||
|
||||
console.log("width", width);
|
||||
console.log("height", height);
|
||||
const x = node.position.x - margin;
|
||||
const y = node.position.y - margin;
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
DraftData,
|
||||
} from "@/services/builder-draft/draft-service";
|
||||
import { BuilderDraft } from "@/lib/dexie/db";
|
||||
import { cleanNodes, cleanEdges } from "@/lib/dexie/draft-utils";
|
||||
import {
|
||||
cleanNodes,
|
||||
cleanEdges,
|
||||
calculateDraftDiff,
|
||||
DraftDiff,
|
||||
} from "@/lib/dexie/draft-utils";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
@@ -19,6 +24,7 @@ const AUTO_SAVE_INTERVAL_MS = 15000; // 15 seconds
|
||||
interface DraftRecoveryState {
|
||||
isOpen: boolean;
|
||||
draft: BuilderDraft | null;
|
||||
diff: DraftDiff | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +37,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
const [state, setState] = useState<DraftRecoveryState>({
|
||||
isOpen: false,
|
||||
draft: null,
|
||||
diff: null,
|
||||
});
|
||||
|
||||
const [{ flowID, flowVersion }] = useQueryStates({
|
||||
@@ -207,9 +214,16 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
);
|
||||
|
||||
if (isDifferent && (draft.nodes.length > 0 || draft.edges.length > 0)) {
|
||||
const diff = calculateDraftDiff(
|
||||
draft.nodes,
|
||||
draft.edges,
|
||||
currentNodes,
|
||||
currentEdges,
|
||||
);
|
||||
setState({
|
||||
isOpen: true,
|
||||
draft,
|
||||
diff,
|
||||
});
|
||||
} else {
|
||||
await draftService.deleteDraft(effectiveFlowId);
|
||||
@@ -231,6 +245,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
setState({
|
||||
isOpen: false,
|
||||
draft: null,
|
||||
diff: null,
|
||||
});
|
||||
}, [flowID]);
|
||||
|
||||
@@ -242,8 +257,10 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
try {
|
||||
useNodeStore.getState().setNodes(draft.nodes);
|
||||
useEdgeStore.getState().setEdges(draft.edges);
|
||||
draft.nodes.forEach((node) => {
|
||||
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
|
||||
});
|
||||
|
||||
// Restore nodeCounter to prevent ID conflicts when adding new nodes
|
||||
if (draft.nodeCounter !== undefined) {
|
||||
useNodeStore.setState({ nodeCounter: draft.nodeCounter });
|
||||
}
|
||||
@@ -267,6 +284,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
setState({
|
||||
isOpen: false,
|
||||
draft: null,
|
||||
diff: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[DraftRecovery] Failed to load draft:", error);
|
||||
@@ -275,7 +293,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
|
||||
const discardDraft = useCallback(async () => {
|
||||
if (!state.draft) {
|
||||
setState({ isOpen: false, draft: null });
|
||||
setState({ isOpen: false, draft: null, diff: null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,7 +303,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
console.error("[DraftRecovery] Failed to discard draft:", error);
|
||||
}
|
||||
|
||||
setState({ isOpen: false, draft: null });
|
||||
setState({ isOpen: false, draft: null, diff: null });
|
||||
}, [state.draft]);
|
||||
|
||||
return {
|
||||
@@ -294,6 +312,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
savedAt: state.draft?.savedAt ?? 0,
|
||||
nodeCount: state.draft?.nodes.length ?? 0,
|
||||
edgeCount: state.draft?.edges.length ?? 0,
|
||||
diff: state.diff,
|
||||
loadDraft,
|
||||
discardDraft,
|
||||
};
|
||||
|
||||
@@ -121,6 +121,14 @@ export const useFlow = () => {
|
||||
if (customNodes.length > 0) {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
addNodes(customNodes);
|
||||
|
||||
// Sync hardcoded values with handle IDs.
|
||||
// If a key–value field has a key without a value, the backend omits it from hardcoded values.
|
||||
// But if a handleId exists for that key, it causes inconsistency.
|
||||
// This ensures hardcoded values stay in sync with handle IDs.
|
||||
customNodes.forEach((node) => {
|
||||
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
|
||||
});
|
||||
}
|
||||
}, [customNodes, addNodes]);
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Connection as RFConnection, EdgeChange } from "@xyflow/react";
|
||||
import {
|
||||
Connection as RFConnection,
|
||||
EdgeChange,
|
||||
applyEdgeChanges,
|
||||
} from "@xyflow/react";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useCallback } from "react";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
|
||||
export const useCustomEdge = () => {
|
||||
const edges = useEdgeStore((s) => s.edges);
|
||||
const addEdge = useEdgeStore((s) => s.addEdge);
|
||||
const removeEdge = useEdgeStore((s) => s.removeEdge);
|
||||
const setEdges = useEdgeStore((s) => s.setEdges);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(conn: RFConnection) => {
|
||||
@@ -45,14 +50,10 @@ export const useCustomEdge = () => {
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "remove") {
|
||||
removeEdge(change.id);
|
||||
}
|
||||
});
|
||||
(changes: EdgeChange<CustomEdge>[]) => {
|
||||
setEdges(applyEdgeChanges(changes, edges));
|
||||
},
|
||||
[removeEdge],
|
||||
[edges, setEdges],
|
||||
);
|
||||
|
||||
return { edges, onConnect, onEdgesChange };
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { CircleIcon } from "@phosphor-icons/react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NodeHandle = ({
|
||||
const InputNodeHandle = ({
|
||||
handleId,
|
||||
isConnected,
|
||||
side,
|
||||
nodeId,
|
||||
}: {
|
||||
handleId: string;
|
||||
isConnected: boolean;
|
||||
side: "left" | "right";
|
||||
nodeId: string;
|
||||
}) => {
|
||||
const cleanedHandleId = cleanUpHandleId(handleId);
|
||||
const isInputConnected = useEdgeStore((state) =>
|
||||
state.isInputConnected(nodeId ?? "", cleanedHandleId),
|
||||
);
|
||||
|
||||
return (
|
||||
<Handle
|
||||
type={side === "left" ? "target" : "source"}
|
||||
position={side === "left" ? Position.Left : Position.Right}
|
||||
id={handleId}
|
||||
className={side === "left" ? "-ml-4 mr-2" : "-mr-2 ml-2"}
|
||||
type={"target"}
|
||||
position={Position.Left}
|
||||
id={cleanedHandleId}
|
||||
className={"-ml-6 mr-2"}
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<CircleIcon
|
||||
size={16}
|
||||
weight={isConnected ? "fill" : "duotone"}
|
||||
weight={isInputConnected ? "fill" : "duotone"}
|
||||
className={"text-gray-400 opacity-100"}
|
||||
/>
|
||||
</div>
|
||||
@@ -28,4 +34,35 @@ const NodeHandle = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeHandle;
|
||||
const OutputNodeHandle = ({
|
||||
field_name,
|
||||
nodeId,
|
||||
hexColor,
|
||||
}: {
|
||||
field_name: string;
|
||||
nodeId: string;
|
||||
hexColor: string;
|
||||
}) => {
|
||||
const isOutputConnected = useEdgeStore((state) =>
|
||||
state.isOutputConnected(nodeId, field_name),
|
||||
);
|
||||
return (
|
||||
<Handle
|
||||
type={"source"}
|
||||
position={Position.Right}
|
||||
id={field_name}
|
||||
className={"-mr-2 ml-2"}
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<CircleIcon
|
||||
size={16}
|
||||
weight={"duotone"}
|
||||
color={isOutputConnected ? hexColor : "gray"}
|
||||
className={cn("text-gray-400 opacity-100")}
|
||||
/>
|
||||
</div>
|
||||
</Handle>
|
||||
);
|
||||
};
|
||||
|
||||
export { InputNodeHandle, OutputNodeHandle };
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
/**
|
||||
* Handle ID Types for different input structures
|
||||
*
|
||||
* Examples:
|
||||
* SIMPLE: "message"
|
||||
* NESTED: "config.api_key"
|
||||
* ARRAY: "items_$_0", "items_$_1"
|
||||
* KEY_VALUE: "headers_#_Authorization", "params_#_limit"
|
||||
*
|
||||
* Note: All handle IDs are sanitized to remove spaces and special characters.
|
||||
* Spaces become underscores, and special characters are removed.
|
||||
* Example: "user name" becomes "user_name", "email@domain.com" becomes "emaildomaincom"
|
||||
*/
|
||||
export enum HandleIdType {
|
||||
SIMPLE = "SIMPLE",
|
||||
NESTED = "NESTED",
|
||||
ARRAY = "ARRAY",
|
||||
KEY_VALUE = "KEY_VALUE",
|
||||
}
|
||||
|
||||
const fromRjsfId = (id: string): string => {
|
||||
if (!id) return "";
|
||||
const parts = id.split("_");
|
||||
const filtered = parts.filter(
|
||||
(p) => p !== "root" && p !== "properties" && p.length > 0,
|
||||
);
|
||||
return filtered.join("_") || "";
|
||||
};
|
||||
// Here we are handling single level of nesting, if need more in future then i will update it
|
||||
|
||||
const sanitizeForHandleId = (str: string): string => {
|
||||
if (!str) return "";
|
||||
@@ -38,51 +11,53 @@ const sanitizeForHandleId = (str: string): string => {
|
||||
.replace(/^_|_$/g, ""); // Remove leading/trailing underscores
|
||||
};
|
||||
|
||||
export const generateHandleId = (
|
||||
const cleanTitleId = (id: string): string => {
|
||||
if (!id) return "";
|
||||
|
||||
if (id.endsWith("_title")) {
|
||||
id = id.slice(0, -6);
|
||||
}
|
||||
const parts = id.split("_");
|
||||
const filtered = parts.filter(
|
||||
(p) => p !== "root" && p !== "properties" && p.length > 0,
|
||||
);
|
||||
const filtered_id = filtered.join("_") || "";
|
||||
return filtered_id;
|
||||
};
|
||||
|
||||
export const generateHandleIdFromTitleId = (
|
||||
fieldKey: string,
|
||||
nestedValues: string[] = [],
|
||||
type: HandleIdType = HandleIdType.SIMPLE,
|
||||
{
|
||||
isObjectProperty,
|
||||
isAdditionalProperty,
|
||||
isArrayItem,
|
||||
}: {
|
||||
isArrayItem?: boolean;
|
||||
isObjectProperty?: boolean;
|
||||
isAdditionalProperty?: boolean;
|
||||
} = {
|
||||
isArrayItem: false,
|
||||
isObjectProperty: false,
|
||||
isAdditionalProperty: false,
|
||||
},
|
||||
): string => {
|
||||
if (!fieldKey) return "";
|
||||
|
||||
fieldKey = fromRjsfId(fieldKey);
|
||||
fieldKey = sanitizeForHandleId(fieldKey);
|
||||
const filteredKey = cleanTitleId(fieldKey);
|
||||
if (isAdditionalProperty || isArrayItem) {
|
||||
return filteredKey;
|
||||
}
|
||||
const cleanedKey = sanitizeForHandleId(filteredKey);
|
||||
|
||||
if (type === HandleIdType.SIMPLE || nestedValues.length === 0) {
|
||||
return fieldKey;
|
||||
if (isObjectProperty) {
|
||||
// "config_api_key" -> "config.api_key"
|
||||
const parts = cleanedKey.split("_");
|
||||
if (parts.length >= 2) {
|
||||
const baseName = parts[0];
|
||||
const propertyName = parts.slice(1).join("_");
|
||||
return `${baseName}.${propertyName}`;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedNestedValues = nestedValues.map((value) =>
|
||||
sanitizeForHandleId(value),
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case HandleIdType.NESTED:
|
||||
return [fieldKey, ...sanitizedNestedValues].join(".");
|
||||
|
||||
case HandleIdType.ARRAY:
|
||||
return [fieldKey, ...sanitizedNestedValues].join("_$_");
|
||||
|
||||
case HandleIdType.KEY_VALUE:
|
||||
return [fieldKey, ...sanitizedNestedValues].join("_#_");
|
||||
|
||||
default:
|
||||
return fieldKey;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseKeyValueHandleId = (
|
||||
handleId: string,
|
||||
type: HandleIdType,
|
||||
): string => {
|
||||
if (type === HandleIdType.KEY_VALUE) {
|
||||
return handleId.split("_#_")[1];
|
||||
} else if (type === HandleIdType.ARRAY) {
|
||||
return handleId.split("_$_")[1];
|
||||
} else if (type === HandleIdType.NESTED) {
|
||||
return handleId.split(".")[1];
|
||||
} else if (type === HandleIdType.SIMPLE) {
|
||||
return handleId.split("_")[1];
|
||||
}
|
||||
return "";
|
||||
return cleanedKey;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutio
|
||||
import { NodeContainer } from "./components/NodeContainer";
|
||||
import { NodeHeader } from "./components/NodeHeader";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle";
|
||||
import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
||||
@@ -99,7 +99,7 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
className={cn(
|
||||
"bg-white pr-6",
|
||||
"bg-white px-4",
|
||||
isWebhook && "pointer-events-none opacity-50",
|
||||
)}
|
||||
showHandles={showHandles}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const NodeAdvancedToggle = ({ nodeId }: { nodeId: string }) => {
|
||||
);
|
||||
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 rounded-b-xlarge border-t border-slate-200/50 bg-white px-5 py-3.5">
|
||||
<div className="flex items-center justify-between gap-2 rounded-b-xlarge border-t border-zinc-200 bg-white px-5 py-3.5">
|
||||
<Text variant="body" className="font-medium text-slate-700">
|
||||
Advanced
|
||||
</Text>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const NodeContainer = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-12 max-w-[370px] rounded-xlarge ring-1 ring-slate-200/60",
|
||||
"z-12 w-[350px] rounded-xlarge ring-1 ring-slate-200/60",
|
||||
selected && "shadow-lg ring-2 ring-slate-200",
|
||||
status && nodeStyleBasedOnStatus[status],
|
||||
hasErrors ? nodeStyleBasedOnStatus[AgentExecutionStatus.FAILED] : "",
|
||||
|
||||
@@ -23,7 +23,9 @@ export const NodeHeader = ({
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
const title = (data.metadata?.customized_name as string) || data.title;
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editedTitle, setEditedTitle] = useState(title);
|
||||
const [editedTitle, setEditedTitle] = useState(
|
||||
beautifyString(title).replace("Block", "").trim(),
|
||||
);
|
||||
|
||||
const handleTitleEdit = () => {
|
||||
updateNodeData(nodeId, {
|
||||
@@ -41,7 +43,7 @@ export const NodeHeader = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-auto flex-col gap-1 rounded-xlarge border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4 pt-3">
|
||||
<div className="flex h-auto flex-col gap-1 rounded-xlarge border-b border-zinc-200 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4 pt-3">
|
||||
{/* Title row with context menu */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
@@ -68,12 +70,12 @@ export const NodeHeader = ({
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Text variant="large-semibold" className="line-clamp-1">
|
||||
{beautifyString(title)}
|
||||
{beautifyString(title).replace("Block", "").trim()}
|
||||
</Text>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{beautifyString(title)}</p>
|
||||
<p>{beautifyString(title).replace("Block", "").trim()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-b-xl border-t border-slate-200/50 px-4 py-4">
|
||||
<div className="flex flex-col gap-3 rounded-b-xl border-t border-zinc-200 px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text variant="body-medium" className="!font-semibold text-slate-700">
|
||||
Node Output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormCreator } from "../../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { uiSchema } from "./uiSchema";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { BlockUIType } from "../../types";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||
|
||||
export const FormCreator = React.memo(
|
||||
({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CaretDownIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
import NodeHandle from "../handlers/NodeHandle";
|
||||
import { OutputNodeHandle } from "../handlers/NodeHandle";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { getTypeDisplayInfo } from "./helpers";
|
||||
import { generateHandleId } from "../handlers/helpers";
|
||||
import { BlockUIType } from "../../types";
|
||||
|
||||
export const OutputHandler = ({
|
||||
@@ -29,8 +28,73 @@ export const OutputHandler = ({
|
||||
const properties = outputSchema?.properties || {};
|
||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||
|
||||
const showHandles = uiType !== BlockUIType.OUTPUT;
|
||||
|
||||
const renderOutputHandles = (
|
||||
schema: RJSFSchema,
|
||||
keyPrefix: string = "",
|
||||
titlePrefix: string = "",
|
||||
): React.ReactNode[] => {
|
||||
return Object.entries(schema).map(
|
||||
([key, fieldSchema]: [string, RJSFSchema]) => {
|
||||
const fullKey = keyPrefix ? `${keyPrefix}_#_${key}` : key;
|
||||
const fieldTitle = titlePrefix + (fieldSchema?.title || key);
|
||||
|
||||
const isConnected = isOutputConnected(nodeId, fullKey);
|
||||
const shouldShow = isConnected || isOutputVisible;
|
||||
const { displayType, colorClass, hexColor } =
|
||||
getTypeDisplayInfo(fieldSchema);
|
||||
|
||||
return shouldShow ? (
|
||||
<div key={fullKey} className="flex flex-col items-end gap-2">
|
||||
<div className="relative flex items-center gap-2">
|
||||
{fieldSchema?.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
style={{ marginLeft: 6, cursor: "pointer" }}
|
||||
aria-label="info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<InfoIcon />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{fieldSchema?.description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Text variant="body" className="text-slate-700">
|
||||
{fieldTitle}
|
||||
</Text>
|
||||
<Text variant="small" as="span" className={colorClass}>
|
||||
({displayType})
|
||||
</Text>
|
||||
|
||||
{showHandles && (
|
||||
<OutputNodeHandle
|
||||
field_name={fullKey}
|
||||
nodeId={nodeId}
|
||||
hexColor={hexColor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recursively render nested properties */}
|
||||
{fieldSchema?.properties &&
|
||||
renderOutputHandles(
|
||||
fieldSchema.properties,
|
||||
fullKey,
|
||||
`${fieldTitle}.`,
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xlarge border-t border-slate-200/50 bg-white py-3.5">
|
||||
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xlarge border-t border-zinc-200 bg-white py-3.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mr-4 h-fit min-w-0 p-0 hover:border-transparent hover:bg-transparent"
|
||||
@@ -49,50 +113,9 @@ export const OutputHandler = ({
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{Object.entries(properties).map(([key, property]: [string, any]) => {
|
||||
const isConnected = isOutputConnected(nodeId, key);
|
||||
const shouldShow = isConnected || isOutputVisible;
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(property);
|
||||
|
||||
return shouldShow ? (
|
||||
<div key={key} className="relative flex items-center gap-2">
|
||||
{property?.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
style={{ marginLeft: 6, cursor: "pointer" }}
|
||||
aria-label="info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<InfoIcon />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{property?.description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Text variant="body" className="text-slate-700">
|
||||
{property?.title || key}{" "}
|
||||
</Text>
|
||||
<Text variant="small" as="span" className={colorClass}>
|
||||
({displayType})
|
||||
</Text>
|
||||
|
||||
<NodeHandle
|
||||
handleId={
|
||||
uiType === BlockUIType.AGENT ? key : generateHandleId(key)
|
||||
}
|
||||
isConnected={isConnected}
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{renderOutputHandles(properties)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -92,14 +92,38 @@ export const getTypeDisplayInfo = (schema: any) => {
|
||||
if (schema?.type === "string" && schema?.format) {
|
||||
const formatMap: Record<
|
||||
string,
|
||||
{ displayType: string; colorClass: string }
|
||||
{ displayType: string; colorClass: string; hexColor: string }
|
||||
> = {
|
||||
file: { displayType: "file", colorClass: "!text-green-500" },
|
||||
date: { displayType: "date", colorClass: "!text-blue-500" },
|
||||
time: { displayType: "time", colorClass: "!text-blue-500" },
|
||||
"date-time": { displayType: "datetime", colorClass: "!text-blue-500" },
|
||||
"long-text": { displayType: "text", colorClass: "!text-green-500" },
|
||||
"short-text": { displayType: "text", colorClass: "!text-green-500" },
|
||||
file: {
|
||||
displayType: "file",
|
||||
colorClass: "!text-green-500",
|
||||
hexColor: "#22c55e",
|
||||
},
|
||||
date: {
|
||||
displayType: "date",
|
||||
colorClass: "!text-blue-500",
|
||||
hexColor: "#3b82f6",
|
||||
},
|
||||
time: {
|
||||
displayType: "time",
|
||||
colorClass: "!text-blue-500",
|
||||
hexColor: "#3b82f6",
|
||||
},
|
||||
"date-time": {
|
||||
displayType: "datetime",
|
||||
colorClass: "!text-blue-500",
|
||||
hexColor: "#3b82f6",
|
||||
},
|
||||
"long-text": {
|
||||
displayType: "text",
|
||||
colorClass: "!text-green-500",
|
||||
hexColor: "#22c55e",
|
||||
},
|
||||
"short-text": {
|
||||
displayType: "text",
|
||||
colorClass: "!text-green-500",
|
||||
hexColor: "#22c55e",
|
||||
},
|
||||
};
|
||||
|
||||
const formatInfo = formatMap[schema.format];
|
||||
@@ -131,10 +155,23 @@ export const getTypeDisplayInfo = (schema: any) => {
|
||||
any: "!text-gray-500",
|
||||
};
|
||||
|
||||
const hexColorMap: Record<string, string> = {
|
||||
string: "#22c55e",
|
||||
number: "#3b82f6",
|
||||
integer: "#3b82f6",
|
||||
boolean: "#eab308",
|
||||
object: "#a855f7",
|
||||
array: "#6366f1",
|
||||
null: "#6b7280",
|
||||
any: "#6b7280",
|
||||
};
|
||||
|
||||
const colorClass = colorMap[schema?.type] || "!text-gray-500";
|
||||
const hexColor = hexColorMap[schema?.type] || "#6b7280";
|
||||
|
||||
return {
|
||||
displayType,
|
||||
colorClass,
|
||||
hexColor,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CustomEdge } from "../components/FlowEditor/edges/CustomEdge";
|
||||
import { customEdgeToLink, linkToCustomEdge } from "../components/helper";
|
||||
import { MarkerType } from "@xyflow/react";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||
|
||||
type EdgeStore = {
|
||||
edges: CustomEdge[];
|
||||
@@ -13,6 +14,8 @@ type EdgeStore = {
|
||||
removeEdge: (edgeId: string) => void;
|
||||
upsertMany: (edges: CustomEdge[]) => void;
|
||||
|
||||
removeEdgesByHandlePrefix: (nodeId: string, handlePrefix: string) => void;
|
||||
|
||||
getNodeEdges: (nodeId: string) => CustomEdge[];
|
||||
isInputConnected: (nodeId: string, handle: string) => boolean;
|
||||
isOutputConnected: (nodeId: string, handle: string) => boolean;
|
||||
@@ -79,11 +82,27 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
return { edges: Array.from(byKey.values()) };
|
||||
}),
|
||||
|
||||
removeEdgesByHandlePrefix: (nodeId, handlePrefix) =>
|
||||
set((state) => ({
|
||||
edges: state.edges.filter(
|
||||
(e) =>
|
||||
!(
|
||||
e.target === nodeId &&
|
||||
e.targetHandle &&
|
||||
e.targetHandle.startsWith(handlePrefix)
|
||||
),
|
||||
),
|
||||
})),
|
||||
|
||||
getNodeEdges: (nodeId) =>
|
||||
get().edges.filter((e) => e.source === nodeId || e.target === nodeId),
|
||||
|
||||
isInputConnected: (nodeId, handle) =>
|
||||
get().edges.some((e) => e.target === nodeId && e.targetHandle === handle),
|
||||
isInputConnected: (nodeId, handle) => {
|
||||
const cleanedHandle = cleanUpHandleId(handle);
|
||||
return get().edges.some(
|
||||
(e) => e.target === nodeId && e.targetHandle === cleanedHandle,
|
||||
);
|
||||
},
|
||||
|
||||
isOutputConnected: (nodeId, handle) =>
|
||||
get().edges.some((e) => e.source === nodeId && e.sourceHandle === handle),
|
||||
@@ -105,15 +124,15 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
targetNodeId: string,
|
||||
executionResult: NodeExecutionResult,
|
||||
) => {
|
||||
set((state) => ({
|
||||
edges: state.edges.map((edge) => {
|
||||
set((state) => {
|
||||
let hasChanges = false;
|
||||
|
||||
const newEdges = state.edges.map((edge) => {
|
||||
if (edge.target !== targetNodeId) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
const beadData =
|
||||
edge.data?.beadData ??
|
||||
new Map<string, NodeExecutionResult["status"]>();
|
||||
const beadData = new Map(edge.data?.beadData ?? new Map());
|
||||
|
||||
const inputValue = edge.targetHandle
|
||||
? executionResult.input_data[edge.targetHandle]
|
||||
@@ -137,6 +156,11 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
beadUp = beadDown + 1;
|
||||
}
|
||||
|
||||
if (edge.data?.beadUp === beadUp && edge.data?.beadDown === beadDown) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
hasChanges = true;
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
@@ -146,8 +170,10 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
beadData,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
return hasChanges ? { edges: newEdges } : state;
|
||||
});
|
||||
},
|
||||
|
||||
resetEdgeBeads: () => {
|
||||
|
||||
@@ -13,6 +13,10 @@ import { useHistoryStore } from "./historyStore";
|
||||
import { useEdgeStore } from "./edgeStore";
|
||||
import { BlockUIType } from "../components/types";
|
||||
import { pruneEmptyValues } from "@/lib/utils";
|
||||
import {
|
||||
ensurePathExists,
|
||||
parseHandleIdToPath,
|
||||
} from "@/components/renderers/InputRenderer/helpers";
|
||||
|
||||
// Minimum movement (in pixels) required before logging position change to history
|
||||
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
||||
@@ -62,6 +66,8 @@ type NodeStore = {
|
||||
errors: { [key: string]: string },
|
||||
) => void;
|
||||
clearAllNodeErrors: () => void; // Add this
|
||||
|
||||
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
|
||||
};
|
||||
|
||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
@@ -305,4 +311,35 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
syncHardcodedValuesWithHandleIds: (nodeId: string) => {
|
||||
const node = get().nodes.find((n) => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const handleIds = useEdgeStore.getState().getAllHandleIdsOfANode(nodeId);
|
||||
const additionalHandles = handleIds.filter((h) => h.includes("_#_"));
|
||||
|
||||
if (additionalHandles.length === 0) return;
|
||||
|
||||
const hardcodedValues = JSON.parse(
|
||||
JSON.stringify(node.data.hardcodedValues || {}),
|
||||
);
|
||||
|
||||
let modified = false;
|
||||
|
||||
additionalHandles.forEach((handleId) => {
|
||||
const segments = parseHandleIdToPath(handleId);
|
||||
if (ensurePathExists(hardcodedValues, segments)) {
|
||||
modified = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, hardcodedValues } } : n,
|
||||
),
|
||||
}));
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -143,6 +143,7 @@ export function CredentialsInput({
|
||||
size="small"
|
||||
onClick={handleActionButtonClick}
|
||||
className="w-fit"
|
||||
type="button"
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
@@ -155,6 +156,7 @@ export function CredentialsInput({
|
||||
size="small"
|
||||
onClick={handleActionButtonClick}
|
||||
className="w-fit"
|
||||
type="button"
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
|
||||
@@ -173,8 +173,9 @@ export function OldAgentLibraryView() {
|
||||
if (agentRuns.length > 0) {
|
||||
// select latest run
|
||||
const latestRun = agentRuns.reduce((latest, current) => {
|
||||
if (latest.started_at && !current.started_at) return current;
|
||||
else if (!latest.started_at) return latest;
|
||||
if (!latest.started_at && !current.started_at) return latest;
|
||||
if (!latest.started_at) return current;
|
||||
if (!current.started_at) return latest;
|
||||
return latest.started_at > current.started_at ? latest : current;
|
||||
}, agentRuns[0]);
|
||||
selectRun(latestRun.id as GraphExecutionID);
|
||||
|
||||
@@ -184,9 +184,11 @@ export function AgentRunsSelectorList({
|
||||
))}
|
||||
{agentPresets.length > 0 && <Separator className="my-1" />}
|
||||
{agentRuns
|
||||
.toSorted(
|
||||
(a, b) => b.started_at.getTime() - a.started_at.getTime(),
|
||||
)
|
||||
.toSorted((a, b) => {
|
||||
const aTime = a.started_at?.getTime() ?? 0;
|
||||
const bTime = b.started_at?.getTime() ?? 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.map((run) => (
|
||||
<AgentRunSummaryCard
|
||||
className={listItemClasses}
|
||||
@@ -199,7 +201,7 @@ export function AgentRunsSelectorList({
|
||||
?.name
|
||||
: null) ?? agent.name
|
||||
}
|
||||
timestamp={run.started_at}
|
||||
timestamp={run.started_at ?? undefined}
|
||||
selected={selectedView.id === run.id}
|
||||
onClick={() => onSelectRun(run.id)}
|
||||
onDelete={() => doDeleteRun(run as GraphExecutionMeta)}
|
||||
|
||||
@@ -120,9 +120,11 @@ export const AgentFlowList = ({
|
||||
lastRun =
|
||||
runCount == 0
|
||||
? null
|
||||
: _flowRuns.reduce((a, c) =>
|
||||
a.started_at > c.started_at ? a : c,
|
||||
);
|
||||
: _flowRuns.reduce((a, c) => {
|
||||
const aTime = a.started_at?.getTime() ?? 0;
|
||||
const cTime = c.started_at?.getTime() ?? 0;
|
||||
return aTime > cTime ? a : c;
|
||||
});
|
||||
}
|
||||
return { flow, runCount, lastRun };
|
||||
})
|
||||
@@ -130,10 +132,9 @@ export const AgentFlowList = ({
|
||||
if (!a.lastRun && !b.lastRun) return 0;
|
||||
if (!a.lastRun) return 1;
|
||||
if (!b.lastRun) return -1;
|
||||
return (
|
||||
b.lastRun.started_at.getTime() -
|
||||
a.lastRun.started_at.getTime()
|
||||
);
|
||||
const bTime = b.lastRun.started_at?.getTime() ?? 0;
|
||||
const aTime = a.lastRun.started_at?.getTime() ?? 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.map(({ flow, runCount, lastRun }) => (
|
||||
<TableRow
|
||||
|
||||
@@ -29,7 +29,10 @@ export const FlowRunsStatus: React.FC<{
|
||||
: statsSince;
|
||||
const filteredFlowRuns =
|
||||
statsSinceTimestamp != null
|
||||
? executions.filter((fr) => fr.started_at.getTime() > statsSinceTimestamp)
|
||||
? executions.filter(
|
||||
(fr) =>
|
||||
fr.started_at && fr.started_at.getTime() > statsSinceTimestamp,
|
||||
)
|
||||
: executions;
|
||||
|
||||
return (
|
||||
|
||||
@@ -98,40 +98,43 @@ export const FlowRunsTimeline = ({
|
||||
<Scatter
|
||||
key={flow.id}
|
||||
data={executions
|
||||
.filter((e) => e.graph_id == flow.graph_id)
|
||||
.filter((e) => e.graph_id == flow.graph_id && e.started_at)
|
||||
.map((e) => ({
|
||||
...e,
|
||||
time:
|
||||
e.started_at.getTime() + (e.stats?.node_exec_time ?? 0) * 1000,
|
||||
(e.started_at?.getTime() ?? 0) +
|
||||
(e.stats?.node_exec_time ?? 0) * 1000,
|
||||
_duration: e.stats?.node_exec_time ?? 0,
|
||||
}))}
|
||||
name={flow.name}
|
||||
fill={`hsl(${(hashString(flow.id) * 137.5) % 360}, 70%, 50%)`}
|
||||
/>
|
||||
))}
|
||||
{executions.map((execution) => (
|
||||
<Line
|
||||
key={execution.id}
|
||||
type="linear"
|
||||
dataKey="_duration"
|
||||
data={[
|
||||
{
|
||||
...execution,
|
||||
time: execution.started_at.getTime(),
|
||||
_duration: 0,
|
||||
},
|
||||
{
|
||||
...execution,
|
||||
time: execution.ended_at.getTime(),
|
||||
_duration: execution.stats?.node_exec_time ?? 0,
|
||||
},
|
||||
]}
|
||||
stroke={`hsl(${(hashString(execution.graph_id) * 137.5) % 360}, 70%, 50%)`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
legendType="none"
|
||||
/>
|
||||
))}
|
||||
{executions
|
||||
.filter((e) => e.started_at && e.ended_at)
|
||||
.map((execution) => (
|
||||
<Line
|
||||
key={execution.id}
|
||||
type="linear"
|
||||
dataKey="_duration"
|
||||
data={[
|
||||
{
|
||||
...execution,
|
||||
time: execution.started_at!.getTime(),
|
||||
_duration: 0,
|
||||
},
|
||||
{
|
||||
...execution,
|
||||
time: execution.ended_at!.getTime(),
|
||||
_duration: execution.stats?.node_exec_time ?? 0,
|
||||
},
|
||||
]}
|
||||
stroke={`hsl(${(hashString(execution.graph_id) * 137.5) % 360}, 70%, 50%)`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
legendType="none"
|
||||
/>
|
||||
))}
|
||||
<Legend
|
||||
content={<ScrollableLegend />}
|
||||
wrapperStyle={{
|
||||
|
||||
@@ -98,7 +98,11 @@ const Monitor = () => {
|
||||
...(selectedFlow
|
||||
? executions.filter((v) => v.graph_id == selectedFlow.graph_id)
|
||||
: executions),
|
||||
].sort((a, b) => b.started_at.getTime() - a.started_at.getTime())}
|
||||
].sort((a, b) => {
|
||||
const aTime = a.started_at?.getTime() ?? 0;
|
||||
const bTime = b.started_at?.getTime() ?? 0;
|
||||
return bTime - aTime;
|
||||
})}
|
||||
selectedRun={selectedRun}
|
||||
onSelectRun={(r) => setSelectedRun(r.id == selectedRun?.id ? null : r)}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { providerIcons } from "@/components/renderers/input-renderer/fields/CredentialField/helpers";
|
||||
import { providerIcons } from "@/components/renderers/InputRenderer/custom/CredentialField/helpers";
|
||||
import { CredentialsProviderName } from "@/lib/autogpt-server-api";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
|
||||
@@ -6968,6 +6968,20 @@
|
||||
"error_message": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Error Message"
|
||||
},
|
||||
"started_at": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At"
|
||||
},
|
||||
"ended_at": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -7074,14 +7088,20 @@
|
||||
},
|
||||
"status": { "$ref": "#/components/schemas/AgentExecutionStatus" },
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Started At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At",
|
||||
"description": "When execution started running. Null if not yet started (QUEUED)."
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Ended At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At",
|
||||
"description": "When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW)."
|
||||
},
|
||||
"is_shared": {
|
||||
"type": "boolean",
|
||||
@@ -7115,8 +7135,6 @@
|
||||
"nodes_input_masks",
|
||||
"preset_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"stats",
|
||||
"outputs"
|
||||
],
|
||||
@@ -7215,14 +7233,20 @@
|
||||
},
|
||||
"status": { "$ref": "#/components/schemas/AgentExecutionStatus" },
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Started At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At",
|
||||
"description": "When execution started running. Null if not yet started (QUEUED)."
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Ended At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At",
|
||||
"description": "When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW)."
|
||||
},
|
||||
"is_shared": {
|
||||
"type": "boolean",
|
||||
@@ -7251,8 +7275,6 @@
|
||||
"nodes_input_masks",
|
||||
"preset_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"stats"
|
||||
],
|
||||
"title": "GraphExecutionMeta"
|
||||
@@ -7299,14 +7321,20 @@
|
||||
},
|
||||
"status": { "$ref": "#/components/schemas/AgentExecutionStatus" },
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Started At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Started At",
|
||||
"description": "When execution started running. Null if not yet started (QUEUED)."
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Ended At"
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Ended At",
|
||||
"description": "When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW)."
|
||||
},
|
||||
"is_shared": {
|
||||
"type": "boolean",
|
||||
@@ -7345,8 +7373,6 @@
|
||||
"nodes_input_masks",
|
||||
"preset_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"stats",
|
||||
"outputs",
|
||||
"node_executions"
|
||||
|
||||
@@ -49,6 +49,7 @@ export function GoogleDrivePicker(props: Props) {
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={handleOpenPicker}
|
||||
disabled={props.disabled || isLoading || isAuthInProgress}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import React, { useCallback } from "react";
|
||||
import { GoogleDrivePicker } from "./GoogleDrivePicker";
|
||||
import { isValidFile } from "./helpers";
|
||||
|
||||
export interface Props {
|
||||
config: GoogleDrivePickerConfig;
|
||||
@@ -27,13 +28,15 @@ export function GoogleDrivePickerInput({
|
||||
const hasAutoCredentials = !!config.auto_credentials;
|
||||
|
||||
// Strip _credentials_id from value for display purposes
|
||||
const currentFiles = isMultiSelect
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: []
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
// Only show files section when there are valid file objects
|
||||
const currentFiles = React.useMemo(() => {
|
||||
if (isMultiSelect) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter(isValidFile);
|
||||
}
|
||||
if (!value || !isValidFile(value)) return [];
|
||||
return [value];
|
||||
}, [value, isMultiSelect]);
|
||||
|
||||
const handlePicked = useCallback(
|
||||
(files: any[], credentialId?: string) => {
|
||||
@@ -85,23 +88,27 @@ export function GoogleDrivePickerInput({
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
{/* Picker Button */}
|
||||
<GoogleDrivePicker
|
||||
multiselect={config.multiselect || false}
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={config.scopes || ["https://www.googleapis.com/auth/drive.file"]}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
{/* Picker Button */}
|
||||
<GoogleDrivePicker
|
||||
multiselect={config.multiselect || false}
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={
|
||||
config.scopes || ["https://www.googleapis.com/auth/drive.file"]
|
||||
}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Selected Files */}
|
||||
{currentFiles.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="mb-8 space-y-1">
|
||||
{currentFiles.map((file: any, idx: number) => (
|
||||
<div
|
||||
key={file.id || idx}
|
||||
|
||||
@@ -119,3 +119,14 @@ export function getCredentialsSchema(scopes: string[]) {
|
||||
secret: true,
|
||||
} satisfies BlockIOCredentialsSubSchema;
|
||||
}
|
||||
|
||||
export function isValidFile(
|
||||
file: unknown,
|
||||
): file is { id?: string; name?: string } {
|
||||
return (
|
||||
typeof file === "object" &&
|
||||
file !== null &&
|
||||
(typeof (file as { id?: unknown }).id === "string" ||
|
||||
typeof (file as { name?: unknown }).name === "string")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ export function ActivityItem({ execution }: Props) {
|
||||
execution.status === AgentExecutionStatus.QUEUED;
|
||||
|
||||
if (isActiveStatus) {
|
||||
const timeAgo = formatTimeAgo(execution.started_at.toString());
|
||||
const timeAgo = execution.started_at
|
||||
? formatTimeAgo(execution.started_at.toString())
|
||||
: "recently";
|
||||
const statusText =
|
||||
execution.status === AgentExecutionStatus.QUEUED ? "queued" : "running";
|
||||
return [
|
||||
@@ -61,7 +63,9 @@ export function ActivityItem({ execution }: Props) {
|
||||
// Handle all other statuses with time display
|
||||
const timeAgo = execution.ended_at
|
||||
? formatTimeAgo(execution.ended_at.toString())
|
||||
: formatTimeAgo(execution.started_at.toString());
|
||||
: execution.started_at
|
||||
? formatTimeAgo(execution.started_at.toString())
|
||||
: "recently";
|
||||
|
||||
let statusText = "ended";
|
||||
switch (execution.status) {
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { BlockUIType } from "@/app/(platform)/build/components/types";
|
||||
import Form from "@rjsf/core";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { fields } from "./fields";
|
||||
import { templates } from "./templates";
|
||||
import { widgets } from "./widgets";
|
||||
import { preprocessInputSchema } from "./utils/input-schema-pre-processor";
|
||||
import { useMemo } from "react";
|
||||
import { customValidator } from "./utils/custom-validator";
|
||||
|
||||
type FormContextType = {
|
||||
nodeId?: string;
|
||||
uiType?: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
};
|
||||
import Form from "./registry";
|
||||
import { ExtendedFormContextType } from "./types";
|
||||
|
||||
type FormRendererProps = {
|
||||
jsonSchema: RJSFSchema;
|
||||
handleChange: (formData: any) => void;
|
||||
uiSchema: any;
|
||||
initialValues: any;
|
||||
formContext: FormContextType;
|
||||
formContext: ExtendedFormContextType;
|
||||
};
|
||||
|
||||
export const FormRenderer = ({
|
||||
@@ -33,19 +23,18 @@ export const FormRenderer = ({
|
||||
const preprocessedSchema = useMemo(() => {
|
||||
return preprocessInputSchema(jsonSchema);
|
||||
}, [jsonSchema]);
|
||||
|
||||
return (
|
||||
<div className={"mt-4"}>
|
||||
<div className={"mb-6 mt-4"}>
|
||||
<Form
|
||||
formContext={formContext}
|
||||
idPrefix="agpt"
|
||||
idSeparator="_%_"
|
||||
schema={preprocessedSchema}
|
||||
validator={customValidator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={formContext}
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
noValidate={true}
|
||||
liveValidate={false}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { FieldProps, getUiOptions, getWidget } from "@rjsf/utils";
|
||||
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
|
||||
import { isEmpty } from "lodash";
|
||||
import { useAnyOfField } from "./useAnyOfField";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { ANY_OF_FLAG } from "../../constants";
|
||||
|
||||
export const AnyOfField = (props: FieldProps) => {
|
||||
const { registry, schema } = props;
|
||||
const { fields } = registry;
|
||||
const { SchemaField: _SchemaField } = fields;
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
|
||||
const uiOptions = getUiOptions(props.uiSchema, props.globalUiOptions);
|
||||
|
||||
const Widget = getWidget({ type: "string" }, "select", registry.widgets);
|
||||
|
||||
const {
|
||||
handleOptionChange,
|
||||
enumOptions,
|
||||
selectedOption,
|
||||
optionSchema,
|
||||
field_id,
|
||||
} = useAnyOfField(props);
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: field_id + ANY_OF_FLAG,
|
||||
schema: schema,
|
||||
});
|
||||
|
||||
const updatedUiSchema = updateUiOption(props.uiSchema, {
|
||||
handleId: handleId,
|
||||
label: false,
|
||||
fromAnyOf: true,
|
||||
});
|
||||
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
const optionsSchemaField =
|
||||
(optionSchema && optionSchema.type !== "null" && (
|
||||
<_SchemaField
|
||||
{...props}
|
||||
schema={optionSchema}
|
||||
uiSchema={updatedUiSchema}
|
||||
/>
|
||||
)) ||
|
||||
null;
|
||||
|
||||
const selector = (
|
||||
<Widget
|
||||
id={field_id}
|
||||
name={`${props.name}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`}
|
||||
schema={{ type: "number", default: 0 }}
|
||||
onChange={handleOptionChange}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
disabled={props.disabled || isEmpty(enumOptions)}
|
||||
multiple={false}
|
||||
value={selectedOption >= 0 ? selectedOption : undefined}
|
||||
options={{ enumOptions }}
|
||||
registry={registry}
|
||||
placeholder={props.placeholder}
|
||||
autocomplete={props.autocomplete}
|
||||
className="-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium"
|
||||
autofocus={props.autofocus}
|
||||
label=""
|
||||
hideLabel={true}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnyOfFieldTitle
|
||||
{...props}
|
||||
selector={selector}
|
||||
uiSchema={updatedUiSchema}
|
||||
/>
|
||||
{!isHandleConnected && optionsSchemaField}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
descriptionId,
|
||||
FieldProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
import { shouldShowTypeSelector } from "../helpers";
|
||||
import { useIsArrayItem } from "../../array/context/array-item-context";
|
||||
import { cleanUpHandleId } from "../../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { isOptionalType } from "../../../utils/schema-utils";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface customFieldProps extends FieldProps {
|
||||
selector: JSX.Element;
|
||||
}
|
||||
|
||||
export const AnyOfFieldTitle = (props: customFieldProps) => {
|
||||
const { uiSchema, schema, required, name, registry, fieldPathId, selector } =
|
||||
props;
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const title_id = titleId(fieldPathId ?? "");
|
||||
const description_id = descriptionId(fieldPathId ?? "");
|
||||
|
||||
const isArrayItem = useIsArrayItem();
|
||||
|
||||
const handleId = cleanUpHandleId(uiOptions.handleId);
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
const { isOptional, type } = isOptionalType(schema); // If we have something like int | null = we will treat it as optional int
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(type);
|
||||
|
||||
const shouldShowSelector =
|
||||
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
|
||||
const shoudlShowType = isHandleConnected || (isOptional && type);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TitleFieldTemplate
|
||||
id={title_id}
|
||||
title={schema.title || name || ""}
|
||||
required={required}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
{shoudlShowType && (
|
||||
<Text variant="small" className={cn("text-zinc-700", colorClass)}>
|
||||
{isOptional ? `(${displayType})` : "(any)"}
|
||||
</Text>
|
||||
)}
|
||||
{shouldShowSelector && selector}
|
||||
<DescriptionFieldTemplate
|
||||
id={description_id}
|
||||
description={schema.description || ""}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { RJSFSchema, StrictRJSFSchema } from "@rjsf/utils";
|
||||
|
||||
const TYPE_PRIORITY = [
|
||||
"string",
|
||||
"number",
|
||||
"integer",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
] as const;
|
||||
|
||||
export function getDefaultTypeIndex(options: StrictRJSFSchema[]): number {
|
||||
for (const preferredType of TYPE_PRIORITY) {
|
||||
const index = options.findIndex((opt) => opt.type === preferredType);
|
||||
if (index >= 0) return index;
|
||||
}
|
||||
|
||||
const nonNullIndex = options.findIndex((opt) => opt.type !== "null");
|
||||
return nonNullIndex >= 0 ? nonNullIndex : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a type selector should be shown for an anyOf schema
|
||||
* Returns false for simple optional types (type | null)
|
||||
* Returns true for complex anyOf (3+ types or multiple non-null types)
|
||||
*/
|
||||
export function shouldShowTypeSelector(
|
||||
schema: RJSFSchema | undefined,
|
||||
): boolean {
|
||||
const anyOf = schema?.anyOf;
|
||||
if (!anyOf || !Array.isArray(anyOf) || anyOf.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anyOf.length === 2 && anyOf.some((opt: any) => opt.type === "null")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return anyOf.length >= 3;
|
||||
}
|
||||
|
||||
export function isSimpleOptional(schema: RJSFSchema | undefined): boolean {
|
||||
const anyOf = schema?.anyOf;
|
||||
return (
|
||||
Array.isArray(anyOf) &&
|
||||
anyOf.length === 2 &&
|
||||
anyOf.some((opt: any) => opt.type === "null")
|
||||
);
|
||||
}
|
||||
|
||||
export function getOptionalType(
|
||||
schema: RJSFSchema | undefined,
|
||||
): string | undefined {
|
||||
if (!isSimpleOptional(schema)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const anyOf = schema?.anyOf;
|
||||
const nonNullOption = anyOf?.find((opt: any) => opt.type !== "null");
|
||||
return nonNullOption ? (nonNullOption as any).type : undefined;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { FieldProps, getFirstMatchingOption, mergeSchemas } from "@rjsf/utils";
|
||||
import { useRef, useState } from "react";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { getDefaultTypeIndex } from "./helpers";
|
||||
import { cleanUpHandleId } from "../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
|
||||
export const useAnyOfField = (props: FieldProps) => {
|
||||
const { registry, schema, options, onChange, formData } = props;
|
||||
const { schemaUtils } = registry;
|
||||
|
||||
const getInitialOption = () => {
|
||||
if (formData !== undefined && formData !== null) {
|
||||
const option = getFirstMatchingOption(
|
||||
validator,
|
||||
formData,
|
||||
options,
|
||||
schema,
|
||||
);
|
||||
return option !== undefined ? option : getDefaultTypeIndex(options);
|
||||
}
|
||||
return getDefaultTypeIndex(options);
|
||||
};
|
||||
|
||||
const [selectedOption, setSelectedOption] =
|
||||
useState<number>(getInitialOption());
|
||||
const retrievedOptions = useRef<any[]>(
|
||||
options.map((opt: any) => schemaUtils.retrieveSchema(opt, formData)),
|
||||
);
|
||||
|
||||
const option =
|
||||
selectedOption >= 0
|
||||
? retrievedOptions.current[selectedOption] || null
|
||||
: null;
|
||||
let optionSchema: any | undefined | null;
|
||||
|
||||
// adding top level required to each option schema
|
||||
if (option) {
|
||||
const { required } = schema;
|
||||
optionSchema = required
|
||||
? (mergeSchemas({ required }, option) as any)
|
||||
: option;
|
||||
}
|
||||
|
||||
const field_id = props.fieldPathId.$id;
|
||||
|
||||
const handleOptionChange = (option?: string) => {
|
||||
const intOption = option !== undefined ? parseInt(option, 10) : -1;
|
||||
if (intOption === selectedOption) return;
|
||||
|
||||
const newOption =
|
||||
intOption >= 0 ? retrievedOptions.current[intOption] : undefined;
|
||||
const oldOption =
|
||||
selectedOption >= 0
|
||||
? retrievedOptions.current[selectedOption]
|
||||
: undefined;
|
||||
|
||||
// When we change the option, we need to clean the form data
|
||||
let newFormData = schemaUtils.sanitizeDataForNewSchema(
|
||||
newOption,
|
||||
oldOption,
|
||||
formData,
|
||||
);
|
||||
|
||||
const handlePrefix = cleanUpHandleId(field_id);
|
||||
console.log("handlePrefix", handlePrefix);
|
||||
useEdgeStore
|
||||
.getState()
|
||||
.removeEdgesByHandlePrefix(registry.formContext.nodeId, handlePrefix);
|
||||
|
||||
// We have cleaned the form data, now we need to get the default form state of new selected option
|
||||
if (newOption) {
|
||||
newFormData = schemaUtils.getDefaultFormState(
|
||||
newOption,
|
||||
newFormData,
|
||||
"excludeObjectChildren",
|
||||
) as any;
|
||||
}
|
||||
|
||||
setSelectedOption(intOption);
|
||||
onChange(newFormData, props.fieldPathId.path, undefined, field_id);
|
||||
};
|
||||
|
||||
const enumOptions = retrievedOptions.current.map((option, index) => ({
|
||||
value: index,
|
||||
label: option.type,
|
||||
}));
|
||||
|
||||
return {
|
||||
handleOptionChange,
|
||||
enumOptions,
|
||||
selectedOption,
|
||||
optionSchema,
|
||||
field_id,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
ArrayFieldItemTemplateProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
export default function ArrayFieldItemTemplate(
|
||||
props: ArrayFieldItemTemplateProps,
|
||||
) {
|
||||
const { children, buttonsProps, hasToolbar, uiSchema, registry } = props;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const ArrayFieldItemButtonsTemplate = getTemplate(
|
||||
"ArrayFieldItemButtonsTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex flex-row flex-wrap items-center">
|
||||
<div className="shrink grow">
|
||||
<div className="shrink grow">{children}</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
{hasToolbar && (
|
||||
<div className="-mt-4 mb-2 flex gap-2">
|
||||
<ArrayFieldItemButtonsTemplate {...buttonsProps} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
ArrayFieldTemplateProps,
|
||||
buttonId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
} from "@rjsf/utils";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
|
||||
export default function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
const {
|
||||
canAdd,
|
||||
disabled,
|
||||
fieldPathId,
|
||||
uiSchema,
|
||||
items,
|
||||
optionalDataControl,
|
||||
onAddClick,
|
||||
readonly,
|
||||
registry,
|
||||
required,
|
||||
schema,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const ArrayFieldDescriptionTemplate = getTemplate(
|
||||
"ArrayFieldDescriptionTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const ArrayFieldTitleTemplate = getTemplate(
|
||||
"ArrayFieldTitleTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const showOptionalDataControlInTitle = !readonly && !disabled;
|
||||
|
||||
const {
|
||||
ButtonTemplates: { AddButton },
|
||||
} = registry.templates;
|
||||
|
||||
const { fromAnyOf } = uiOptions;
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldPathId.$id,
|
||||
schema: schema,
|
||||
});
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="m-0 flex p-0">
|
||||
<div className="m-0 w-full space-y-4 p-0">
|
||||
{!fromAnyOf && (
|
||||
<div className="flex items-center">
|
||||
<ArrayFieldTitleTemplate
|
||||
fieldPathId={fieldPathId}
|
||||
title={uiOptions.title || title}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
required={required}
|
||||
registry={registry}
|
||||
optionalDataControl={
|
||||
showOptionalDataControlInTitle
|
||||
? optionalDataControl
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ArrayFieldDescriptionTemplate
|
||||
fieldPathId={fieldPathId}
|
||||
description={uiOptions.description || schema.description}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
key={`array-item-list-${fieldPathId.$id}`}
|
||||
className="m-0 mb-2 w-full p-0"
|
||||
>
|
||||
{!showOptionalDataControlInTitle ? optionalDataControl : undefined}
|
||||
{items}
|
||||
{canAdd && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<AddButton
|
||||
id={buttonId(fieldPathId, "add")}
|
||||
className="rjsf-array-item-add"
|
||||
onClick={onAddClick}
|
||||
disabled={disabled || readonly}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FieldProps, getUiOptions } from "@rjsf/utils";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import { ARRAY_ITEM_FLAG } from "../../constants";
|
||||
|
||||
const ArraySchemaField = (props: FieldProps) => {
|
||||
const { index, registry, fieldPathId } = props;
|
||||
const { SchemaField } = registry.fields;
|
||||
|
||||
const uiOptions = getUiOptions(props.uiSchema);
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldPathId.$id,
|
||||
schema: props.schema,
|
||||
});
|
||||
const updatedUiSchema = updateUiOption(props.uiSchema, {
|
||||
handleId: handleId + ARRAY_ITEM_FLAG,
|
||||
});
|
||||
|
||||
return (
|
||||
<SchemaField
|
||||
{...props}
|
||||
uiSchema={updatedUiSchema}
|
||||
title={"_item-" + index.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArraySchemaField;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
interface ArrayItemContextValue {
|
||||
isArrayItem: boolean;
|
||||
arrayItemHandleId: string;
|
||||
}
|
||||
|
||||
const ArrayItemContext = createContext<ArrayItemContextValue>({
|
||||
isArrayItem: false,
|
||||
arrayItemHandleId: "",
|
||||
});
|
||||
|
||||
export const ArrayItemProvider: React.FC<{
|
||||
children: React.ReactNode;
|
||||
arrayItemHandleId: string;
|
||||
}> = ({ children, arrayItemHandleId }) => {
|
||||
return (
|
||||
<ArrayItemContext.Provider value={{ isArrayItem: true, arrayItemHandleId }}>
|
||||
{children}
|
||||
</ArrayItemContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useIsArrayItem = (): boolean => {
|
||||
// here this will be true if field is inside an array
|
||||
const context = useContext(ArrayItemContext);
|
||||
return context.isArrayItem;
|
||||
};
|
||||
|
||||
export const useArrayItemHandleId = (): string => {
|
||||
const context = useContext(ArrayItemContext);
|
||||
return context.arrayItemHandleId;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const generateArrayItemHandleId = (id: string) => {
|
||||
return `array-item-${id}`;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as ArrayFieldTemplate } from "./ArrayFieldTemplate";
|
||||
export { default as ArrayFieldItemTemplate } from "./ArrayFieldItemTemplate";
|
||||
export { default as ArraySchemaField } from "./ArraySchemaField";
|
||||
export {
|
||||
ArrayItemProvider,
|
||||
useIsArrayItem,
|
||||
} from "./context/array-item-context";
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
RegistryFieldsType,
|
||||
RegistryWidgetsType,
|
||||
TemplatesType,
|
||||
} from "@rjsf/utils";
|
||||
import { AnyOfField } from "./anyof/AnyOfField";
|
||||
import {
|
||||
ArrayFieldItemTemplate,
|
||||
ArrayFieldTemplate,
|
||||
ArraySchemaField,
|
||||
} from "./array";
|
||||
import {
|
||||
ObjectFieldTemplate,
|
||||
OptionalDataControlsTemplate,
|
||||
WrapIfAdditionalTemplate,
|
||||
} from "./object";
|
||||
import { DescriptionField, FieldTemplate, TitleField } from "./standard";
|
||||
import { AddButton, CopyButton, RemoveButton } from "./standard/buttons";
|
||||
import {
|
||||
CheckboxWidget,
|
||||
DateTimeWidget,
|
||||
DateWidget,
|
||||
FileWidget,
|
||||
GoogleDrivePickerWidget,
|
||||
SelectWidget,
|
||||
TextWidget,
|
||||
TimeWidget,
|
||||
} from "./standard/widgets";
|
||||
|
||||
const NoButton = () => null;
|
||||
|
||||
export function generateBaseFields(): RegistryFieldsType {
|
||||
return {
|
||||
AnyOfField,
|
||||
ArraySchemaField,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBaseTemplates(): Partial<TemplatesType> {
|
||||
return {
|
||||
ArrayFieldItemTemplate,
|
||||
ArrayFieldTemplate,
|
||||
ButtonTemplates: {
|
||||
AddButton,
|
||||
CopyButton,
|
||||
MoveDownButton: NoButton,
|
||||
MoveUpButton: NoButton,
|
||||
RemoveButton,
|
||||
SubmitButton: NoButton,
|
||||
},
|
||||
DescriptionFieldTemplate: DescriptionField,
|
||||
FieldTemplate,
|
||||
ObjectFieldTemplate,
|
||||
OptionalDataControlsTemplate,
|
||||
TitleFieldTemplate: TitleField,
|
||||
WrapIfAdditionalTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBaseWidgets(): RegistryWidgetsType {
|
||||
return {
|
||||
TextWidget,
|
||||
SelectWidget,
|
||||
CheckboxWidget,
|
||||
FileWidget,
|
||||
DateWidget,
|
||||
TimeWidget,
|
||||
DateTimeWidget,
|
||||
GoogleDrivePickerWidget,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./array";
|
||||
export * from "./object";
|
||||
export * from "./standard";
|
||||
export * from "./standard/widgets";
|
||||
export * from "./standard/buttons";
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
buttonId,
|
||||
canExpand,
|
||||
descriptionId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
ObjectFieldTemplateProps,
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import React from "react";
|
||||
|
||||
export default function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
const {
|
||||
description,
|
||||
title,
|
||||
properties,
|
||||
required,
|
||||
uiSchema,
|
||||
fieldPathId,
|
||||
schema,
|
||||
formData,
|
||||
optionalDataControl,
|
||||
onAddProperty,
|
||||
disabled,
|
||||
readonly,
|
||||
registry,
|
||||
} = props;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const showOptionalDataControlInTitle = !readonly && !disabled;
|
||||
|
||||
const {
|
||||
ButtonTemplates: { AddButton },
|
||||
} = registry.templates;
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldPathId.$id,
|
||||
schema,
|
||||
});
|
||||
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
{title && !additional && (
|
||||
<TitleFieldTemplate
|
||||
id={titleId(fieldPathId)}
|
||||
title={title}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
optionalDataControl={true ? optionalDataControl : undefined}
|
||||
/>
|
||||
)}
|
||||
{description && (
|
||||
<DescriptionFieldTemplate
|
||||
id={descriptionId(fieldPathId)}
|
||||
description={description}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{!showOptionalDataControlInTitle ? optionalDataControl : undefined}
|
||||
|
||||
{/* I have cloned it - so i could pass updated uiSchema to the nested children */}
|
||||
{properties.map((element: any, index: number) => {
|
||||
const clonedContent = React.cloneElement(element.content, {
|
||||
...element.content.props,
|
||||
uiSchema: updateUiOption(element.content.props.uiSchema, {
|
||||
handleId: handleId,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`${element.hidden ? "hidden" : ""} flex`}
|
||||
>
|
||||
<div className="w-full">{clonedContent}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{canExpand(schema, uiSchema, formData) ? (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<AddButton
|
||||
id={buttonId(fieldPathId, "add")}
|
||||
onClick={onAddProperty}
|
||||
disabled={disabled || readonly}
|
||||
className="rjsf-object-property-expand"
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { OptionalDataControlsTemplateProps } from "@rjsf/utils";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
|
||||
import { IconButton, RemoveButton } from "../standard/buttons";
|
||||
|
||||
export default function OptionalDataControlsTemplate(
|
||||
props: OptionalDataControlsTemplateProps,
|
||||
) {
|
||||
const { id, registry, label, onAddClick, onRemoveClick } = props;
|
||||
if (onAddClick) {
|
||||
return (
|
||||
<IconButton
|
||||
id={id}
|
||||
registry={registry}
|
||||
className="rjsf-add-optional-data"
|
||||
onClick={onAddClick}
|
||||
title={label}
|
||||
icon={<PlusCircle />}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
} else if (onRemoveClick) {
|
||||
return (
|
||||
<RemoveButton
|
||||
id={id}
|
||||
registry={registry}
|
||||
className="rjsf-remove-optional-data"
|
||||
onClick={onRemoveClick}
|
||||
title={label}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <em id={id}>{label}</em>;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
buttonId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId,
|
||||
WrapIfAdditionalTemplateProps,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
|
||||
export default function WrapIfAdditionalTemplate(
|
||||
props: WrapIfAdditionalTemplateProps,
|
||||
) {
|
||||
const {
|
||||
classNames,
|
||||
style,
|
||||
children,
|
||||
disabled,
|
||||
id,
|
||||
label,
|
||||
onRemoveProperty,
|
||||
onKeyRenameBlur,
|
||||
readonly,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
registry,
|
||||
} = props;
|
||||
const { templates, formContext } = registry;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
// Button templates are not overridden in the uiSchema
|
||||
const { RemoveButton } = templates.ButtonTemplates;
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
const { nodeId } = formContext;
|
||||
const handleId = uiOptions.handleId;
|
||||
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
if (!additional) {
|
||||
return (
|
||||
<div className={classNames} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const keyId = `${id}-key`;
|
||||
const generateObjectPropertyTitleId = (id: string, label: string) => {
|
||||
return id.replace(`_${label}`, `_#_${label}`);
|
||||
};
|
||||
const title_id = generateObjectPropertyTitleId(id, label);
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (e.target.value == "") {
|
||||
onRemoveProperty();
|
||||
} else {
|
||||
onKeyRenameBlur(e);
|
||||
}
|
||||
};
|
||||
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`mb-4 flex flex-col gap-1`} style={style}>
|
||||
<TitleFieldTemplate
|
||||
id={titleId(title_id)}
|
||||
title={`#${label}`}
|
||||
required={required}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
{!isHandleConnected && (
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<Input
|
||||
label={""}
|
||||
hideLabel={true}
|
||||
required={required}
|
||||
defaultValue={label}
|
||||
disabled={disabled || readonly}
|
||||
id={keyId}
|
||||
wrapperClassName="mb-2 w-30"
|
||||
name={keyId}
|
||||
onBlur={!readonly ? handleBlur : undefined}
|
||||
type="text"
|
||||
size="small"
|
||||
/>
|
||||
<div className="mt-2"> {children}</div>
|
||||
</div>
|
||||
)}
|
||||
{!isHandleConnected && (
|
||||
<div className="-mt-4">
|
||||
<RemoveButton
|
||||
id={buttonId(id, "remove")}
|
||||
disabled={disabled || readonly}
|
||||
onClick={onRemoveProperty}
|
||||
uiSchema={uiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as ObjectFieldTemplate } from "./ObjectFieldTemplate";
|
||||
export { default as WrapIfAdditionalTemplate } from "./WrapIfAdditionalTemplate";
|
||||
export { default as OptionalDataControlsTemplate } from "./OptionalDataControlsTemplate";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DescriptionFieldProps } from "@rjsf/utils";
|
||||
import { RichDescription } from "@rjsf/core";
|
||||
import { InfoIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
|
||||
export default function DescriptionField(props: DescriptionFieldProps) {
|
||||
const { id, description, registry, uiSchema } = props;
|
||||
if (!description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={id} className="0 inline w-fit">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon size={16} className="cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<RichDescription
|
||||
description={description}
|
||||
registry={registry}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export const FieldError = ({
|
||||
nodeId,
|
||||
fieldId,
|
||||
}: {
|
||||
nodeId: string;
|
||||
fieldId: string;
|
||||
}) => {
|
||||
const nodeErrors = useNodeStore((state) => {
|
||||
const node = state.nodes.find((n) => n.id === nodeId);
|
||||
return node?.data?.errors;
|
||||
});
|
||||
const fieldError =
|
||||
nodeErrors?.[fieldId] || nodeErrors?.[fieldId.replace(/_%_/g, ".")] || null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fieldError && (
|
||||
<Text variant="small" className="mt-1 pl-4 !text-red-600">
|
||||
{fieldError}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
FieldTemplateProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { isAnyOfChild, isAnyOfSchema } from "../../utils/schema-utils";
|
||||
import {
|
||||
cleanUpHandleId,
|
||||
getHandleId,
|
||||
isPartOfAnyOf,
|
||||
updateUiOption,
|
||||
} from "../../helpers";
|
||||
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { FieldError } from "./FieldError";
|
||||
|
||||
export default function FieldTemplate(props: FieldTemplateProps) {
|
||||
const {
|
||||
id,
|
||||
children,
|
||||
displayLabel,
|
||||
description,
|
||||
rawDescription,
|
||||
label,
|
||||
hidden,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
registry,
|
||||
classNames,
|
||||
style,
|
||||
disabled,
|
||||
onKeyRename,
|
||||
onKeyRenameBlur,
|
||||
onRemoveProperty,
|
||||
readonly,
|
||||
} = props;
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[registry.formContext.nodeId ?? ""],
|
||||
);
|
||||
|
||||
if (hidden) {
|
||||
return <div className="hidden">{children}</div>;
|
||||
}
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const WrapIfAdditionalTemplate = getTemplate(
|
||||
"WrapIfAdditionalTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: id,
|
||||
schema: schema,
|
||||
});
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
const isHandleConnected = isInputConnected(nodeId, cleanUpHandleId(handleId));
|
||||
|
||||
const shouldDisplayLabel =
|
||||
displayLabel ||
|
||||
(schema.type === "boolean" && !isAnyOfChild(uiSchema as any));
|
||||
const shouldShowTitleSection = !isAnyOfSchema(schema) && !additional;
|
||||
const shouldShowChildren = isAnyOfSchema(schema) || !isHandleConnected;
|
||||
|
||||
const isAdvancedField = (schema as any).advanced === true;
|
||||
if (!showAdvanced && isAdvancedField && !isHandleConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marginBottom =
|
||||
isPartOfAnyOf({ uiOptions }) || isAnyOfSchema(schema) ? 0 : 16;
|
||||
|
||||
return (
|
||||
<WrapIfAdditionalTemplate
|
||||
classNames={classNames}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
label={label}
|
||||
displayLabel={displayLabel}
|
||||
onKeyRename={onKeyRename}
|
||||
onKeyRenameBlur={onKeyRenameBlur}
|
||||
onRemoveProperty={onRemoveProperty}
|
||||
rawDescription={rawDescription}
|
||||
readonly={readonly}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
>
|
||||
<div className="flex flex-col gap-2" style={{ marginBottom }}>
|
||||
{shouldShowTitleSection && (
|
||||
<div className="flex items-center gap-2">
|
||||
{shouldDisplayLabel && (
|
||||
<TitleFieldTemplate
|
||||
id={titleId(id)}
|
||||
title={label}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
)}
|
||||
{shouldDisplayLabel && rawDescription && <span>{description}</span>}
|
||||
</div>
|
||||
)}
|
||||
{shouldShowChildren && children}
|
||||
|
||||
<FieldError nodeId={nodeId} fieldId={cleanUpHandleId(id)} />
|
||||
</div>
|
||||
</WrapIfAdditionalTemplate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
descriptionId,
|
||||
getUiOptions,
|
||||
TitleFieldProps,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { isAnyOfSchema } from "../../utils/schema-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isArrayItem } from "../../helpers";
|
||||
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||
|
||||
export default function TitleField(props: TitleFieldProps) {
|
||||
const { id, title, required, schema, registry, uiSchema } = props;
|
||||
const { nodeId, showHandles } = registry.formContext;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const isAnyOf = isAnyOfSchema(schema);
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(schema);
|
||||
const description_id = descriptionId(id);
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
const isArrayItemFlag = isArrayItem({ uiOptions });
|
||||
const smallText = isArrayItemFlag || additional;
|
||||
|
||||
const showHandle = uiOptions.showHandles ?? showHandles;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{showHandle !== false && (
|
||||
<InputNodeHandle handleId={uiOptions.handleId} nodeId={nodeId} />
|
||||
)}
|
||||
<Text
|
||||
variant={isArrayItemFlag ? "small" : "body"}
|
||||
id={id}
|
||||
className={cn("line-clamp-1", smallText && "text-sm text-zinc-700")}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text variant="small" className={"mr-1 text-red-500"}>
|
||||
{required ? "*" : null}
|
||||
</Text>
|
||||
{!isAnyOf && (
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn("ml-2", colorClass)}
|
||||
id={description_id}
|
||||
>
|
||||
({displayType})
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconButtonProps, TranslatableString } from "@rjsf/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
|
||||
export default function AddButton({
|
||||
registry,
|
||||
className,
|
||||
uiSchema: _uiSchema,
|
||||
...props
|
||||
}: IconButtonProps) {
|
||||
const { translateString } = registry;
|
||||
return (
|
||||
<div className="m-0 w-full p-0">
|
||||
<Button
|
||||
{...props}
|
||||
size="small"
|
||||
className={cn("w-full gap-4", className)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon size={16} weight="bold" />
|
||||
{translateString(TranslatableString.AddItemButton)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
FormContextType,
|
||||
IconButtonProps,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
TranslatableString,
|
||||
} from "@rjsf/utils";
|
||||
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { extendedButtonVariants } from "@/components/atoms/Button/helpers";
|
||||
import { TrashIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export type AutogptIconButtonProps<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
> = IconButtonProps<T, S, F> & VariantProps<typeof extendedButtonVariants>;
|
||||
|
||||
export default function IconButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
icon,
|
||||
className,
|
||||
uiSchema: _uiSchema,
|
||||
registry: _registry,
|
||||
iconType: _iconType,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className={cn(className, "w-fit border border-zinc-200 p-1.5 px-4")}
|
||||
{...otherProps}
|
||||
type="button"
|
||||
>
|
||||
{icon}
|
||||
<Text variant="body" className="ml-2">
|
||||
{" "}
|
||||
Remove Item{" "}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.CopyButton)}
|
||||
{...props}
|
||||
icon={<Copy className="h-4 w-4" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveDownButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.MoveDownButton)}
|
||||
{...props}
|
||||
icon={<ChevronDown className="h-4 w-4" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveUpButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.MoveUpButton)}
|
||||
{...props}
|
||||
icon={<ChevronUp className="h-4 w-4" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RemoveButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.RemoveButton)}
|
||||
{...props}
|
||||
className={"border-destructive"}
|
||||
icon={<TrashIcon size={16} className="!text-zinc-800" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as AddButton } from "./AddButton";
|
||||
export {
|
||||
default as IconButton,
|
||||
CopyButton,
|
||||
RemoveButton,
|
||||
MoveUpButton,
|
||||
MoveDownButton,
|
||||
} from "./IconButton";
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ErrorListProps, TranslatableString } from "@rjsf/utils";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/molecules/Alert/Alert";
|
||||
|
||||
export default function ErrorList(props: ErrorListProps) {
|
||||
const { errors, registry } = props;
|
||||
const { translateString } = registry;
|
||||
return (
|
||||
<Alert variant="error" className="mb-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>{translateString(TranslatableString.ErrorsLabel)}</AlertTitle>
|
||||
<AlertDescription className="flex flex-col gap-1">
|
||||
{errors.map((error, i: number) => {
|
||||
return <span key={i}>• {error.stack}</span>;
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ErrorList } from "./ErrorList";
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
|
||||
export function parseFieldPath(
|
||||
rootSchema: RJSFSchema,
|
||||
id: string,
|
||||
additional: boolean,
|
||||
idSeparator: string = "_%_",
|
||||
): { path: string[]; typeHints: string[] } {
|
||||
const segments = id.split(idSeparator).filter(Boolean);
|
||||
const typeHints: string[] = [];
|
||||
|
||||
let currentSchema = rootSchema;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
const isNumeric = /^\d+$/.test(segment);
|
||||
|
||||
if (isNumeric) {
|
||||
typeHints.push("array");
|
||||
} else {
|
||||
if (additional) {
|
||||
typeHints.push("object-key");
|
||||
} else {
|
||||
typeHints.push("object-property");
|
||||
}
|
||||
currentSchema = (currentSchema.properties?.[segment] as RJSFSchema) || {};
|
||||
}
|
||||
}
|
||||
|
||||
return { path: segments, typeHints };
|
||||
}
|
||||
|
||||
// This helper work is simple - it just help us to convert rjsf id to our backend compatible id
|
||||
// Example : List[dict] = agpt_%_List_0_dict__title -> List_$_0_#_dict
|
||||
// We remove the prefix and suffix and then we split id by our custom delimiter (_%_)
|
||||
// then add _$_ delimiter for array and _#_ delimiter for object-key
|
||||
// and for normal property we add . delimiter
|
||||
|
||||
export function getHandleId(
|
||||
rootSchema: RJSFSchema,
|
||||
id: string,
|
||||
additional: boolean,
|
||||
idSeparator: string = "_%_",
|
||||
): string {
|
||||
const idPrefix = "agpt_%_";
|
||||
const idSuffix = "__title";
|
||||
|
||||
if (id.startsWith(idPrefix)) {
|
||||
id = id.slice(idPrefix.length);
|
||||
}
|
||||
if (id.endsWith(idSuffix)) {
|
||||
id = id.slice(0, -idSuffix.length);
|
||||
}
|
||||
|
||||
const { path, typeHints } = parseFieldPath(
|
||||
rootSchema,
|
||||
id,
|
||||
additional,
|
||||
idSeparator,
|
||||
);
|
||||
|
||||
return path
|
||||
.map((seg, i) => {
|
||||
const type = typeHints[i];
|
||||
if (type === "array") {
|
||||
return `_$_${seg}`;
|
||||
}
|
||||
if (type === "object-key") {
|
||||
return `_${seg}`; // we haven't added _#_ delimiter for object-key because it's already added in the id - check WrapIfAdditionalTemplate.tsx
|
||||
}
|
||||
|
||||
return `.${seg}`;
|
||||
})
|
||||
.join("")
|
||||
.slice(1);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as FieldTemplate } from "./FieldTemplate";
|
||||
export { default as TitleField } from "./TitleField";
|
||||
export { default as DescriptionField } from "./DescriptionField";
|
||||
@@ -1,8 +1,9 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
|
||||
export function SwitchWidget(props: WidgetProps) {
|
||||
export function CheckboxWidget(props: WidgetProps) {
|
||||
const { value = false, onChange, disabled, readonly, autofocus, id } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id={id}
|
||||
@@ -0,0 +1 @@
|
||||
export { CheckboxWidget } from "./CheckBoxWidget";
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { DateInput } from "@/components/atoms/DateInput/DateInput";
|
||||
|
||||
export const DateInputWidget = (props: WidgetProps) => {
|
||||
export const DateWidget = (props: WidgetProps) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
@@ -0,0 +1 @@
|
||||
export { DateWidget } from "./DateWidget";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { DateTimeInput } from "@/components/atoms/DateTimeInput/DateTimeInput";
|
||||
|
||||
export const DateTimeInputWidget = (props: WidgetProps) => {
|
||||
export const DateTimeWidget = (props: WidgetProps) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
@@ -0,0 +1 @@
|
||||
export { DateTimeWidget } from "./DateTimeWidget";
|
||||
@@ -0,0 +1 @@
|
||||
export { FileWidget } from "./FileWidget";
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
|
||||
import { getFieldErrorKey } from "@/components/renderers/InputRenderer/utils/helpers";
|
||||
import type { GoogleDrivePickerConfig } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
|
||||
function hasGoogleDrivePickerConfig(
|
||||
schema: unknown,
|
||||
): schema is { google_drive_picker_config?: GoogleDrivePickerConfig } {
|
||||
return (
|
||||
typeof schema === "object" &&
|
||||
schema !== null &&
|
||||
"google_drive_picker_config" in schema
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleDrivePickerWidget(props: WidgetProps) {
|
||||
const { onChange, disabled, readonly, value, schema, id, formContext } =
|
||||
props;
|
||||
const { nodeId } = formContext || {};
|
||||
|
||||
const nodeErrors = useNodeStore((state) => {
|
||||
const node = state.nodes.find((n) => n.id === nodeId);
|
||||
return node?.data?.errors;
|
||||
});
|
||||
|
||||
const fieldErrorKey = getFieldErrorKey(id ?? "");
|
||||
const fieldError =
|
||||
nodeErrors?.[fieldErrorKey] ||
|
||||
nodeErrors?.[fieldErrorKey.replace(/_/g, ".")] ||
|
||||
nodeErrors?.[fieldErrorKey.replace(/\./g, "_")] ||
|
||||
undefined;
|
||||
|
||||
const config: GoogleDrivePickerConfig = hasGoogleDrivePickerConfig(schema)
|
||||
? schema.google_drive_picker_config || {}
|
||||
: {};
|
||||
|
||||
function handleChange(newValue: unknown) {
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<GoogleDrivePickerInput
|
||||
config={config}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
error={fieldError}
|
||||
className={cn(
|
||||
disabled || readonly ? "pointer-events-none opacity-50" : undefined,
|
||||
)}
|
||||
showRemoveButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GoogleDrivePickerWidget } from "./GoogleDrivePicketWidget";
|
||||
@@ -14,8 +14,16 @@ import {
|
||||
} from "@/components/__legacy__/ui/multiselect";
|
||||
|
||||
export const SelectWidget = (props: WidgetProps) => {
|
||||
const { options, value, onChange, disabled, readonly, id, formContext } =
|
||||
props;
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
readonly,
|
||||
className,
|
||||
id,
|
||||
formContext,
|
||||
} = props;
|
||||
const enumOptions = options.enumOptions || [];
|
||||
const type = mapJsonSchemaTypeToInputType(props.schema);
|
||||
const { size = "small" } = formContext || {};
|
||||
@@ -36,7 +44,7 @@ export const SelectWidget = (props: WidgetProps) => {
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{enumOptions?.map((option) => (
|
||||
{enumOptions?.map((option: any) => (
|
||||
<MultiSelectorItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MultiSelectorItem>
|
||||
@@ -56,12 +64,13 @@ export const SelectWidget = (props: WidgetProps) => {
|
||||
value={value ?? ""}
|
||||
onValueChange={onChange}
|
||||
options={
|
||||
enumOptions?.map((option) => ({
|
||||
enumOptions?.map((option: any) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
})) || []
|
||||
}
|
||||
wrapperClassName="!mb-0 "
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SelectWidget } from "./SelectWidget";
|
||||
@@ -14,15 +14,12 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
import { InputExpanderModal } from "./InputExpanderModal";
|
||||
import { ArrowsOutIcon } from "@phosphor-icons/react";
|
||||
import { InputExpanderModal } from "./TextInputExpanderModal";
|
||||
|
||||
export const TextInputWidget = (props: WidgetProps) => {
|
||||
const { schema, formContext } = props;
|
||||
const { uiType, size = "small" } = formContext as {
|
||||
uiType: BlockUIType;
|
||||
size?: string;
|
||||
};
|
||||
export default function TextWidget(props: WidgetProps) {
|
||||
const { schema, placeholder, registry } = props;
|
||||
const { size, uiType } = registry.formContext;
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
@@ -51,7 +48,7 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
handleChange: (v: string) => (v === "" ? undefined : Number(v)),
|
||||
},
|
||||
[InputType.INTEGER]: {
|
||||
htmlType: "number",
|
||||
htmlType: "account",
|
||||
placeholder: "Enter integer value...",
|
||||
handleChange: (v: string) => (v === "" ? undefined : Number(v)),
|
||||
},
|
||||
@@ -122,7 +119,7 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
wrapperClassName="mb-0 flex-1"
|
||||
value={props.value ?? ""}
|
||||
onChange={handleChange}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
placeholder={placeholder || config.placeholder}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
className={showExpandButton ? "pr-8" : ""}
|
||||
@@ -152,8 +149,8 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
title={schema.title || "Edit value"}
|
||||
description={schema.description || ""}
|
||||
defaultValue={props.value ?? ""}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
placeholder={placeholder || config.placeholder}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./TextWidget";
|
||||
export { InputExpanderModal } from "./TextInputExpanderModal";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { TimeInput } from "@/components/atoms/TimeInput/TimeInput";
|
||||
|
||||
export const TimeInputWidget = (props: WidgetProps) => {
|
||||
export const TimeWidget = (props: WidgetProps) => {
|
||||
const { value, onChange, disabled, readonly, placeholder, id, formContext } =
|
||||
props;
|
||||
const { size = "small" } = formContext || {};
|
||||
@@ -0,0 +1 @@
|
||||
export { TimeWidget } from "./TimeWidget";
|
||||
@@ -0,0 +1,8 @@
|
||||
export { CheckboxWidget } from "./CheckboxInput";
|
||||
export { DateWidget } from "./DateInput";
|
||||
export { DateTimeWidget } from "./DateTimeInput";
|
||||
export { FileWidget } from "./FileInput";
|
||||
export { GoogleDrivePickerWidget } from "./GoogleDrivePicker";
|
||||
export { SelectWidget } from "./SelectInput";
|
||||
export { default as TextWidget } from "./TextInput";
|
||||
export { TimeWidget } from "./TimeInput";
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ANY_OF_FLAG = "__anyOf";
|
||||
export const ARRAY_FLAG = "__array";
|
||||
export const OBJECT_FLAG = "__object";
|
||||
export const KEY_PAIR_FLAG = "__keyPair";
|
||||
export const TITLE_FLAG = "__title";
|
||||
export const ARRAY_ITEM_FLAG = "__arrayItem";
|
||||
export const ID_PREFIX = "agpt_@_";
|
||||
export const ID_PREFIX_ARRAY = "agpt_%_";
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { FieldProps, getUiOptions } from "@rjsf/utils";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
CredentialsMetaInput,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { CredentialFieldTitle } from "./components/CredentialFieldTitle";
|
||||
|
||||
export const CredentialsField = (props: FieldProps) => {
|
||||
const { formData, onChange, schema, registry, fieldPathId } = props;
|
||||
|
||||
const formContext = registry.formContext;
|
||||
const uiOptions = getUiOptions(props.uiSchema);
|
||||
const nodeId = formContext?.nodeId;
|
||||
|
||||
// Get sibling inputs (hardcoded values) from the node store
|
||||
const hardcodedValues = useNodeStore(
|
||||
useShallow((state) => (nodeId ? state.getHardCodedValues(nodeId) : {})),
|
||||
);
|
||||
|
||||
const handleChange = (newValue: any) => {
|
||||
onChange(newValue, fieldPathId?.path);
|
||||
};
|
||||
|
||||
const handleSelectCredentials = (credentialsMeta?: CredentialsMetaInput) => {
|
||||
if (credentialsMeta) {
|
||||
handleChange({
|
||||
id: credentialsMeta.id,
|
||||
provider: credentialsMeta.provider,
|
||||
title: credentialsMeta.title,
|
||||
type: credentialsMeta.type,
|
||||
});
|
||||
} else {
|
||||
handleChange(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Convert formData to CredentialsMetaInput format
|
||||
const selectedCredentials: CredentialsMetaInput | undefined = useMemo(
|
||||
() =>
|
||||
formData?.id
|
||||
? {
|
||||
id: formData.id,
|
||||
provider: formData.provider,
|
||||
title: formData.title,
|
||||
type: formData.type,
|
||||
}
|
||||
: undefined,
|
||||
[formData?.id, formData?.provider, formData?.title, formData?.type],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<CredentialFieldTitle
|
||||
fieldPathId={fieldPathId}
|
||||
registry={registry}
|
||||
uiOptions={uiOptions}
|
||||
schema={schema}
|
||||
/>
|
||||
<CredentialsInput
|
||||
schema={schema as BlockIOCredentialsSubSchema}
|
||||
selectedCredentials={selectedCredentials}
|
||||
onSelectCredentials={handleSelectCredentials}
|
||||
siblingInputs={hardcodedValues}
|
||||
showTitle={false}
|
||||
readOnly={formContext?.readOnly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user