mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): Implement UI for Agent Input subtypes (#9700)
- Follow-up to #9657 <img width="280" alt="image" src="https://github.com/user-attachments/assets/2f3cd683-db63-485f-8914-5654c34f1a4c" /> <img width="520" alt="image" src="https://github.com/user-attachments/assets/de7e7cb9-61d4-4071-aea8-393ff5200c54" /> ### Changes 🏗️ * Implement the input UI for Agent Input subtypes. * Refactor node-input-component, extra out data type decision logic, share it with runner/library input. * Add `format` field for short-text, long-text, and mediafile type. * Unify UI data type enum. Out of scope: - Styling for these inputs. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Use all the available agent input subtypes in an agent and run it --------- Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
This commit is contained in:
@@ -4,19 +4,19 @@ from typing import Any, List
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util import json
|
||||
from backend.util.file import MediaFile, store_media_file
|
||||
from backend.util.file import store_media_file
|
||||
from backend.util.mock import MockObject
|
||||
from backend.util.type import convert
|
||||
from backend.util.type import MediaFileType, convert
|
||||
|
||||
|
||||
class FileStoreBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
file_in: MediaFile = SchemaField(
|
||||
file_in: MediaFileType = SchemaField(
|
||||
description="The file to store in the temporary directory, it can be a URL, data URI, or local path."
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
file_out: MediaFile = SchemaField(
|
||||
file_out: MediaFileType = SchemaField(
|
||||
description="The relative path to the stored file in the temporary directory."
|
||||
)
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ from typing import Any, Optional
|
||||
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.file import MediaFile, store_media_file
|
||||
from backend.util.file import store_media_file
|
||||
from backend.util.mock import MockObject
|
||||
from backend.util.settings import Config
|
||||
from backend.util.text import TextFormatter
|
||||
from backend.util.type import LongTextType, MediaFileType, ShortTextType
|
||||
|
||||
formatter = TextFormatter()
|
||||
config = Config()
|
||||
@@ -39,11 +40,7 @@ class AgentInputBlock(Block):
|
||||
description="The placeholder values to be passed as input.",
|
||||
default=[],
|
||||
advanced=True,
|
||||
)
|
||||
limit_to_placeholder_values: bool = SchemaField(
|
||||
description="Whether to limit the selection to placeholder values.",
|
||||
default=False,
|
||||
advanced=True,
|
||||
hidden=True,
|
||||
)
|
||||
advanced: bool = SchemaField(
|
||||
description="Whether to show the input in the advanced section, if the field is not required.",
|
||||
@@ -56,6 +53,12 @@ class AgentInputBlock(Block):
|
||||
advanced=True,
|
||||
)
|
||||
|
||||
def generate_schema(self):
|
||||
schema = self.get_field_schema("value")
|
||||
if possible_values := self.placeholder_values:
|
||||
schema["enum"] = possible_values
|
||||
return schema
|
||||
|
||||
class Output(BlockSchema):
|
||||
result: Any = SchemaField(description="The value passed as input.")
|
||||
|
||||
@@ -72,14 +75,12 @@ class AgentInputBlock(Block):
|
||||
"name": "input_1",
|
||||
"description": "Example test input.",
|
||||
"placeholder_values": [],
|
||||
"limit_to_placeholder_values": False,
|
||||
},
|
||||
{
|
||||
"value": "Hello, World!",
|
||||
"name": "input_2",
|
||||
"description": "Example test input with placeholders.",
|
||||
"placeholder_values": ["Hello, World!"],
|
||||
"limit_to_placeholder_values": True,
|
||||
},
|
||||
],
|
||||
"test_output": [
|
||||
@@ -141,6 +142,9 @@ class AgentOutputBlock(Block):
|
||||
advanced=True,
|
||||
)
|
||||
|
||||
def generate_schema(self):
|
||||
return self.get_field_schema("value")
|
||||
|
||||
class Output(BlockSchema):
|
||||
output: Any = SchemaField(description="The value recorded as output.")
|
||||
name: Any = SchemaField(description="The name of the value recorded as output.")
|
||||
@@ -200,12 +204,11 @@ class AgentOutputBlock(Block):
|
||||
|
||||
class AgentShortTextInputBlock(AgentInputBlock):
|
||||
class Input(AgentInputBlock.Input):
|
||||
value: Optional[str] = SchemaField(
|
||||
value: Optional[ShortTextType] = SchemaField(
|
||||
description="Short text input.",
|
||||
default=None,
|
||||
advanced=False,
|
||||
title="Default Value",
|
||||
json_schema_extra={"format": "short-text"},
|
||||
)
|
||||
|
||||
class Output(AgentInputBlock.Output):
|
||||
@@ -224,14 +227,12 @@ class AgentShortTextInputBlock(AgentInputBlock):
|
||||
"name": "short_text_1",
|
||||
"description": "Short text example 1",
|
||||
"placeholder_values": [],
|
||||
"limit_to_placeholder_values": False,
|
||||
},
|
||||
{
|
||||
"value": "Quick test",
|
||||
"name": "short_text_2",
|
||||
"description": "Short text example 2",
|
||||
"placeholder_values": ["Quick test", "Another option"],
|
||||
"limit_to_placeholder_values": True,
|
||||
},
|
||||
],
|
||||
test_output=[
|
||||
@@ -243,12 +244,11 @@ class AgentShortTextInputBlock(AgentInputBlock):
|
||||
|
||||
class AgentLongTextInputBlock(AgentInputBlock):
|
||||
class Input(AgentInputBlock.Input):
|
||||
value: Optional[str] = SchemaField(
|
||||
value: Optional[LongTextType] = SchemaField(
|
||||
description="Long text input (potentially multi-line).",
|
||||
default=None,
|
||||
advanced=False,
|
||||
title="Default Value",
|
||||
json_schema_extra={"format": "long-text"},
|
||||
)
|
||||
|
||||
class Output(AgentInputBlock.Output):
|
||||
@@ -267,14 +267,12 @@ class AgentLongTextInputBlock(AgentInputBlock):
|
||||
"name": "long_text_1",
|
||||
"description": "Long text example 1",
|
||||
"placeholder_values": [],
|
||||
"limit_to_placeholder_values": False,
|
||||
},
|
||||
{
|
||||
"value": "Another multiline text input.",
|
||||
"name": "long_text_2",
|
||||
"description": "Long text example 2",
|
||||
"placeholder_values": ["Another multiline text input."],
|
||||
"limit_to_placeholder_values": True,
|
||||
},
|
||||
],
|
||||
test_output=[
|
||||
@@ -309,14 +307,12 @@ class AgentNumberInputBlock(AgentInputBlock):
|
||||
"name": "number_input_1",
|
||||
"description": "Number example 1",
|
||||
"placeholder_values": [],
|
||||
"limit_to_placeholder_values": False,
|
||||
},
|
||||
{
|
||||
"value": 314,
|
||||
"name": "number_input_2",
|
||||
"description": "Number example 2",
|
||||
"placeholder_values": [314, 2718],
|
||||
"limit_to_placeholder_values": True,
|
||||
},
|
||||
],
|
||||
test_output=[
|
||||
@@ -410,7 +406,7 @@ class AgentFileInputBlock(AgentInputBlock):
|
||||
"""
|
||||
|
||||
class Input(AgentInputBlock.Input):
|
||||
value: Optional[MediaFile] = SchemaField(
|
||||
value: Optional[MediaFileType] = SchemaField(
|
||||
description="Path or reference to an uploaded file.",
|
||||
default=None,
|
||||
advanced=False,
|
||||
@@ -459,8 +455,7 @@ class AgentFileInputBlock(AgentInputBlock):
|
||||
|
||||
class AgentDropdownInputBlock(AgentInputBlock):
|
||||
"""
|
||||
A specialized text input block that relies on placeholder_values +
|
||||
limit_to_placeholder_values to present a dropdown.
|
||||
A specialized text input block that relies on placeholder_values to present a dropdown.
|
||||
"""
|
||||
|
||||
class Input(AgentInputBlock.Input):
|
||||
@@ -476,10 +471,6 @@ class AgentDropdownInputBlock(AgentInputBlock):
|
||||
advanced=False,
|
||||
title="Dropdown Options",
|
||||
)
|
||||
limit_to_placeholder_values: bool = SchemaField(
|
||||
description="Whether the selection is limited to placeholder values.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
class Output(AgentInputBlock.Output):
|
||||
result: str = SchemaField(description="Selected dropdown value.")
|
||||
@@ -496,14 +487,12 @@ class AgentDropdownInputBlock(AgentInputBlock):
|
||||
"value": "Option A",
|
||||
"name": "dropdown_1",
|
||||
"placeholder_values": ["Option A", "Option B", "Option C"],
|
||||
"limit_to_placeholder_values": True,
|
||||
"description": "Dropdown example 1",
|
||||
},
|
||||
{
|
||||
"value": "Option C",
|
||||
"name": "dropdown_2",
|
||||
"placeholder_values": ["Option A", "Option B", "Option C"],
|
||||
"limit_to_placeholder_values": True,
|
||||
"description": "Dropdown example 2",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -8,13 +8,13 @@ from moviepy.video.io.VideoFileClip import VideoFileClip
|
||||
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.file import MediaFile, get_exec_file_path, store_media_file
|
||||
from backend.util.file import MediaFileType, get_exec_file_path, store_media_file
|
||||
|
||||
|
||||
class MediaDurationBlock(Block):
|
||||
|
||||
class Input(BlockSchema):
|
||||
media_in: MediaFile = SchemaField(
|
||||
media_in: MediaFileType = SchemaField(
|
||||
description="Media input (URL, data URI, or local path)."
|
||||
)
|
||||
is_video: bool = SchemaField(
|
||||
@@ -69,7 +69,7 @@ class LoopVideoBlock(Block):
|
||||
"""
|
||||
|
||||
class Input(BlockSchema):
|
||||
video_in: MediaFile = SchemaField(
|
||||
video_in: MediaFileType = SchemaField(
|
||||
description="The input video (can be a URL, data URI, or local path)."
|
||||
)
|
||||
# Provide EITHER a `duration` or `n_loops` or both. We'll demonstrate `duration`.
|
||||
@@ -137,7 +137,7 @@ class LoopVideoBlock(Block):
|
||||
assert isinstance(looped_clip, VideoFileClip)
|
||||
|
||||
# 4) Save the looped output
|
||||
output_filename = MediaFile(
|
||||
output_filename = MediaFileType(
|
||||
f"{node_exec_id}_looped_{os.path.basename(local_video_path)}"
|
||||
)
|
||||
output_abspath = get_exec_file_path(graph_exec_id, output_filename)
|
||||
@@ -162,10 +162,10 @@ class AddAudioToVideoBlock(Block):
|
||||
"""
|
||||
|
||||
class Input(BlockSchema):
|
||||
video_in: MediaFile = SchemaField(
|
||||
video_in: MediaFileType = SchemaField(
|
||||
description="Video input (URL, data URI, or local path)."
|
||||
)
|
||||
audio_in: MediaFile = SchemaField(
|
||||
audio_in: MediaFileType = SchemaField(
|
||||
description="Audio input (URL, data URI, or local path)."
|
||||
)
|
||||
volume: float = SchemaField(
|
||||
@@ -178,7 +178,7 @@ class AddAudioToVideoBlock(Block):
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
video_out: MediaFile = SchemaField(
|
||||
video_out: MediaFileType = SchemaField(
|
||||
description="Final video (with attached audio), as a path or data URI."
|
||||
)
|
||||
error: str = SchemaField(
|
||||
@@ -229,7 +229,7 @@ class AddAudioToVideoBlock(Block):
|
||||
final_clip = video_clip.with_audio(audio_clip)
|
||||
|
||||
# 4) Write to output file
|
||||
output_filename = MediaFile(
|
||||
output_filename = MediaFileType(
|
||||
f"{node_exec_id}_audio_attached_{os.path.basename(local_video_path)}"
|
||||
)
|
||||
output_abspath = os.path.join(abs_temp_dir, output_filename)
|
||||
|
||||
@@ -6,13 +6,14 @@ from backend.blocks.nvidia._auth import (
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.request import requests
|
||||
from backend.util.type import MediaFileType
|
||||
|
||||
|
||||
class NvidiaDeepfakeDetectBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
credentials: NvidiaCredentialsInput = NvidiaCredentialsField()
|
||||
image_base64: str = SchemaField(
|
||||
description="Image to analyze for deepfakes", image_upload=True
|
||||
image_base64: MediaFileType = SchemaField(
|
||||
description="Image to analyze for deepfakes",
|
||||
)
|
||||
return_image: bool = SchemaField(
|
||||
description="Whether to return the processed image with markings",
|
||||
@@ -22,16 +23,12 @@ class NvidiaDeepfakeDetectBlock(Block):
|
||||
class Output(BlockSchema):
|
||||
status: str = SchemaField(
|
||||
description="Detection status (SUCCESS, ERROR, CONTENT_FILTERED)",
|
||||
default="",
|
||||
)
|
||||
image: str = SchemaField(
|
||||
image: MediaFileType = SchemaField(
|
||||
description="Processed image with detection markings (if return_image=True)",
|
||||
default="",
|
||||
image_output=True,
|
||||
)
|
||||
is_deepfake: float = SchemaField(
|
||||
description="Probability that the image is a deepfake (0-1)",
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -12,7 +12,7 @@ from backend.data.model import (
|
||||
SchemaField,
|
||||
)
|
||||
from backend.integrations.providers import ProviderName
|
||||
from backend.util.file import MediaFile, store_media_file
|
||||
from backend.util.file import MediaFileType, store_media_file
|
||||
from backend.util.request import Requests
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class ScreenshotWebPageBlock(Block):
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
image: MediaFile = SchemaField(description="The screenshot image data")
|
||||
image: MediaFileType = SchemaField(description="The screenshot image data")
|
||||
error: str = SchemaField(description="Error message if the screenshot failed")
|
||||
|
||||
def __init__(self):
|
||||
@@ -142,7 +142,7 @@ class ScreenshotWebPageBlock(Block):
|
||||
return {
|
||||
"image": store_media_file(
|
||||
graph_exec_id=graph_exec_id,
|
||||
file=MediaFile(
|
||||
file=MediaFileType(
|
||||
f"data:image/{format.value};base64,{b64encode(response.content).decode('utf-8')}"
|
||||
),
|
||||
return_content=True,
|
||||
|
||||
@@ -120,21 +120,26 @@ class BlockSchema(BaseModel):
|
||||
def get_mismatch_error(cls, data: BlockInput) -> str | None:
|
||||
return cls.validate_data(data)
|
||||
|
||||
@classmethod
|
||||
def get_field_schema(cls, field_name: str) -> dict[str, Any]:
|
||||
model_schema = cls.jsonschema().get("properties", {})
|
||||
if not model_schema:
|
||||
raise ValueError(f"Invalid model schema {cls}")
|
||||
|
||||
property_schema = model_schema.get(field_name)
|
||||
if not property_schema:
|
||||
raise ValueError(f"Invalid property name {field_name}")
|
||||
|
||||
return property_schema
|
||||
|
||||
@classmethod
|
||||
def validate_field(cls, field_name: str, data: BlockInput) -> str | None:
|
||||
"""
|
||||
Validate the data against a specific property (one of the input/output name).
|
||||
Returns the validation error message if the data does not match the schema.
|
||||
"""
|
||||
model_schema = cls.jsonschema().get("properties", {})
|
||||
if not model_schema:
|
||||
return f"Invalid model schema {cls}"
|
||||
|
||||
property_schema = model_schema.get(field_name)
|
||||
if not property_schema:
|
||||
return f"Invalid property name {field_name}"
|
||||
|
||||
try:
|
||||
property_schema = cls.get_field_schema(field_name)
|
||||
jsonschema.validate(json.to_dict(data), property_schema)
|
||||
return None
|
||||
except jsonschema.ValidationError as e:
|
||||
|
||||
@@ -165,46 +165,48 @@ class BaseGraph(BaseDbModel):
|
||||
@property
|
||||
def input_schema(self) -> dict[str, Any]:
|
||||
return self._generate_schema(
|
||||
AgentInputBlock.Input,
|
||||
[
|
||||
node.input_default
|
||||
*(
|
||||
(b.input_schema, node.input_default)
|
||||
for node in self.nodes
|
||||
if (b := get_block(node.block_id))
|
||||
and b.block_type == BlockType.INPUT
|
||||
and "name" in node.input_default
|
||||
],
|
||||
and issubclass(b.input_schema, AgentInputBlock.Input)
|
||||
)
|
||||
)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def output_schema(self) -> dict[str, Any]:
|
||||
return self._generate_schema(
|
||||
AgentOutputBlock.Input,
|
||||
[
|
||||
node.input_default
|
||||
*(
|
||||
(b.input_schema, node.input_default)
|
||||
for node in self.nodes
|
||||
if (b := get_block(node.block_id))
|
||||
and b.block_type == BlockType.OUTPUT
|
||||
and "name" in node.input_default
|
||||
],
|
||||
and issubclass(b.input_schema, AgentOutputBlock.Input)
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _generate_schema(
|
||||
type_class: Type[AgentInputBlock.Input] | Type[AgentOutputBlock.Input],
|
||||
data: list[dict],
|
||||
*props: tuple[Type[AgentInputBlock.Input] | Type[AgentOutputBlock.Input], dict],
|
||||
) -> dict[str, Any]:
|
||||
props = []
|
||||
for p in data:
|
||||
schema = []
|
||||
for type_class, input_default in props:
|
||||
try:
|
||||
props.append(type_class(**p))
|
||||
schema.append(type_class(**input_default))
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid {type_class}: {p}, {e}")
|
||||
logger.warning(f"Invalid {type_class}: {input_default}, {e}")
|
||||
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
p.name: {
|
||||
**{
|
||||
k: v
|
||||
for k, v in p.generate_schema().items()
|
||||
if k not in ["description", "default"]
|
||||
},
|
||||
"secret": p.secret,
|
||||
# Default value has to be set for advanced fields.
|
||||
"advanced": p.advanced and p.value is not None,
|
||||
@@ -212,9 +214,9 @@ class BaseGraph(BaseDbModel):
|
||||
**({"description": p.description} if p.description else {}),
|
||||
**({"default": p.value} if p.value is not None else {}),
|
||||
}
|
||||
for p in props
|
||||
for p in schema
|
||||
},
|
||||
"required": [p.name for p in props if p.value is None],
|
||||
"required": [p.name for p in schema if p.value is None],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -141,10 +141,8 @@ def SchemaField(
|
||||
secret: bool = False,
|
||||
exclude: bool = False,
|
||||
hidden: Optional[bool] = None,
|
||||
depends_on: list[str] | None = None,
|
||||
image_upload: Optional[bool] = None,
|
||||
image_output: Optional[bool] = None,
|
||||
json_schema_extra: dict[str, Any] | None = None,
|
||||
depends_on: Optional[list[str]] = None,
|
||||
json_schema_extra: Optional[dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
if default is PydanticUndefined and default_factory is None:
|
||||
@@ -160,8 +158,6 @@ def SchemaField(
|
||||
"advanced": advanced,
|
||||
"hidden": hidden,
|
||||
"depends_on": depends_on,
|
||||
"image_upload": image_upload,
|
||||
"image_output": image_output,
|
||||
**(json_schema_extra or {}),
|
||||
}.items()
|
||||
if v is not None
|
||||
|
||||
@@ -7,8 +7,8 @@ import uuid
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# This "requests" presumably has additional checks against internal networks for SSRF.
|
||||
from backend.util.request import requests
|
||||
from backend.util.type import MediaFileType
|
||||
|
||||
TEMP_DIR = Path(tempfile.gettempdir()).resolve()
|
||||
|
||||
@@ -29,30 +29,9 @@ def clean_exec_files(graph_exec_id: str, file: str = "") -> None:
|
||||
shutil.rmtree(exec_path)
|
||||
|
||||
|
||||
class MediaFile(str):
|
||||
"""
|
||||
MediaFile is a string that represents a file. It can be one of the following:
|
||||
- Data URI: base64 encoded media file. See https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data/
|
||||
- URL: Media file hosted on the internet, it starts with http:// or https://.
|
||||
- Local path (anything else): A temporary file path living within graph execution time.
|
||||
|
||||
Note: Replace this type alias into a proper class, when more information is needed.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(cls, source_type, handler):
|
||||
return handler(str)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(cls, core_schema, handler):
|
||||
json_schema = handler(core_schema)
|
||||
json_schema["format"] = "file"
|
||||
return json_schema
|
||||
|
||||
|
||||
def store_media_file(
|
||||
graph_exec_id: str, file: MediaFile, return_content: bool = False
|
||||
) -> MediaFile:
|
||||
graph_exec_id: str, file: MediaFileType, return_content: bool = False
|
||||
) -> MediaFileType:
|
||||
"""
|
||||
Safely handle 'file' (a data URI, a URL, or a local path relative to {temp}/exec_file/{exec_id}),
|
||||
placing or verifying it under:
|
||||
@@ -61,7 +40,7 @@ def store_media_file(
|
||||
If 'return_content=True', return a data URI (data:<mime>;base64,<content>).
|
||||
Otherwise, returns the file media path relative to the exec_id folder.
|
||||
|
||||
For each MediaFile type:
|
||||
For each MediaFileType type:
|
||||
- Data URI:
|
||||
-> decode and store in a new random file in that folder
|
||||
- URL:
|
||||
@@ -148,6 +127,6 @@ def store_media_file(
|
||||
|
||||
# Return result
|
||||
if return_content:
|
||||
return MediaFile(_file_to_data_uri(target_path))
|
||||
return MediaFileType(_file_to_data_uri(target_path))
|
||||
else:
|
||||
return MediaFile(_strip_base_prefix(target_path, base_path))
|
||||
return MediaFileType(_strip_base_prefix(target_path, base_path))
|
||||
|
||||
@@ -228,7 +228,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
description="Whether to use the new agent image generation service",
|
||||
)
|
||||
enable_agent_input_subtype_blocks: bool = Field(
|
||||
default=False,
|
||||
default=True,
|
||||
description="Whether to enable the agent input subtype blocks",
|
||||
)
|
||||
|
||||
|
||||
@@ -195,3 +195,38 @@ def convert(value: Any, target_type: Type[T]) -> T:
|
||||
return cast(T, _try_convert(value, target_type, raise_on_mismatch=False))
|
||||
except Exception as e:
|
||||
raise ConversionError(f"Failed to convert {value} to {target_type}") from e
|
||||
|
||||
|
||||
class FormattedStringType(str):
|
||||
string_format: str
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(cls, source_type, handler):
|
||||
return handler(str)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(cls, core_schema, handler):
|
||||
json_schema = handler(core_schema)
|
||||
json_schema["format"] = cls.string_format
|
||||
return json_schema
|
||||
|
||||
|
||||
class MediaFileType(FormattedStringType):
|
||||
"""
|
||||
MediaFile is a string that represents a file. It can be one of the following:
|
||||
- Data URI: base64 encoded media file. See https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data/
|
||||
- URL: Media file hosted on the internet, it starts with http:// or https://.
|
||||
- Local path (anything else): A temporary file path living within graph execution time.
|
||||
|
||||
Note: Replace this type alias into a proper class, when more information is needed.
|
||||
"""
|
||||
|
||||
string_format = "file"
|
||||
|
||||
|
||||
class LongTextType(FormattedStringType):
|
||||
string_format = "long-text"
|
||||
|
||||
|
||||
class ShortTextType(FormattedStringType):
|
||||
string_format = "short-text"
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
@layer components {
|
||||
.agpt-border-input {
|
||||
@apply border border-input focus-visible:border-gray-400 focus-visible:outline-none;
|
||||
@apply m-0.5 border border-input focus-visible:border-gray-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-400;
|
||||
}
|
||||
|
||||
.agpt-shadow-input {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
|
||||
import { FC, memo, useCallback } from "react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
@@ -11,6 +12,7 @@ type HandleProps = {
|
||||
isRequired?: boolean;
|
||||
side: "left" | "right";
|
||||
title?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// Move the constant out of the component to avoid re-creation on every render.
|
||||
@@ -46,6 +48,7 @@ const NodeHandle: FC<HandleProps> = ({
|
||||
isRequired,
|
||||
side,
|
||||
title,
|
||||
className,
|
||||
}) => {
|
||||
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${
|
||||
side === "left" ? "text-left" : "text-right"
|
||||
@@ -53,7 +56,12 @@ const NodeHandle: FC<HandleProps> = ({
|
||||
|
||||
const label = (
|
||||
<div className="flex flex-grow flex-row">
|
||||
<span className="text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100">
|
||||
<span
|
||||
className={cn(
|
||||
"text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{title || schema.title || beautifyString(keyName.toLowerCase())}
|
||||
{isRequired ? "*" : ""}
|
||||
</span>
|
||||
|
||||
@@ -8,7 +8,11 @@ import RunnerOutputUI, { BlockOutput } from "./runner-ui/RunnerOutputUI";
|
||||
import RunnerInputUI from "./runner-ui/RunnerInputUI";
|
||||
import { Node } from "@xyflow/react";
|
||||
import { filterBlocksByType } from "@/lib/utils";
|
||||
import { BlockIORootSchema, BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
BlockIOObjectSubSchema,
|
||||
BlockIORootSchema,
|
||||
BlockUIType,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
|
||||
interface HardcodedValues {
|
||||
@@ -16,7 +20,6 @@ interface HardcodedValues {
|
||||
description: any;
|
||||
value: any;
|
||||
placeholder_values: any;
|
||||
limit_to_placeholder_values: any;
|
||||
}
|
||||
|
||||
export interface InputItem {
|
||||
@@ -80,16 +83,14 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
({
|
||||
id: node.id,
|
||||
type: "input" as const,
|
||||
inputSchema: node.data.inputSchema as BlockIORootSchema,
|
||||
inputSchema: (node.data.inputSchema as BlockIOObjectSubSchema)
|
||||
.properties.value as BlockIORootSchema,
|
||||
hardcodedValues: {
|
||||
name: (node.data.hardcodedValues as any).name || "",
|
||||
description: (node.data.hardcodedValues as any).description || "",
|
||||
value: (node.data.hardcodedValues as any).value,
|
||||
placeholder_values:
|
||||
(node.data.hardcodedValues as any).placeholder_values || [],
|
||||
limit_to_placeholder_values:
|
||||
(node.data.hardcodedValues as any)
|
||||
.limit_to_placeholder_values || false,
|
||||
},
|
||||
}) satisfies InputItem,
|
||||
);
|
||||
@@ -111,7 +112,7 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
}, [nodes]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(nodeId: string, field: string, value: string) => {
|
||||
(nodeId: string, field: string, value: any) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
|
||||
@@ -6,18 +6,11 @@ import { GraphExecutionID, GraphMeta } from "@/lib/autogpt-server-api";
|
||||
|
||||
import type { ButtonAction } from "@/components/agptui/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { LocalValuedInput } from "@/components/ui/input";
|
||||
import { TypeBasedInput } from "@/components/type-based-input";
|
||||
import { useToastOnFail } from "@/components/ui/use-toast";
|
||||
import { Pencil2Icon } from "@radix-ui/react-icons";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import SchemaTooltip from "@/components/SchemaTooltip";
|
||||
import { IconPlay } from "@/components/ui/icons";
|
||||
import { Button } from "@/components/agptui/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function AgentRunDraftView({
|
||||
graph,
|
||||
@@ -33,27 +26,6 @@ export default function AgentRunDraftView({
|
||||
|
||||
const agentInputs = graph.input_schema.properties;
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>({});
|
||||
const [expandedInputKey, setExpandedInputKey] = useState<string | null>(null);
|
||||
const [tempInputValue, setTempInputValue] = useState("");
|
||||
|
||||
const openInputPopout = useCallback(
|
||||
(key: string) => {
|
||||
setTempInputValue(inputValues[key] || "");
|
||||
setExpandedInputKey(key);
|
||||
},
|
||||
[inputValues],
|
||||
);
|
||||
|
||||
const closeInputPopout = useCallback(() => {
|
||||
setExpandedInputKey(null);
|
||||
}, []);
|
||||
|
||||
const saveAndCloseInputPopout = useCallback(() => {
|
||||
if (!expandedInputKey) return;
|
||||
|
||||
setInputValues((obj) => ({ ...obj, [expandedInputKey]: tempInputValue }));
|
||||
closeInputPopout();
|
||||
}, [expandedInputKey, tempInputValue, closeInputPopout]);
|
||||
|
||||
const doRun = useCallback(
|
||||
() =>
|
||||
@@ -89,68 +61,29 @@ export default function AgentRunDraftView({
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{Object.entries(agentInputs).map(([key, inputSubSchema]) => (
|
||||
<div key={key} className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">
|
||||
<div key={key} className="flex flex-col space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
{inputSubSchema.title || key}
|
||||
<SchemaTooltip description={inputSubSchema.description} />
|
||||
</label>
|
||||
|
||||
<div className="nodrag relative">
|
||||
<LocalValuedInput
|
||||
// TODO: render specific inputs based on input types
|
||||
defaultValue={
|
||||
"default" in inputSubSchema ? inputSubSchema.default : ""
|
||||
}
|
||||
value={inputValues[key] ?? undefined}
|
||||
className="rounded-full pr-8"
|
||||
onChange={(e) =>
|
||||
setInputValues((obj) => ({
|
||||
...obj,
|
||||
[key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute inset-1 left-auto h-7 w-8 rounded-full"
|
||||
onClick={() => openInputPopout(key)}
|
||||
title="Open a larger textbox input"
|
||||
>
|
||||
<Pencil2Icon className="m-0 p-0" />
|
||||
</Button>
|
||||
</div>
|
||||
<TypeBasedInput
|
||||
schema={inputSubSchema}
|
||||
value={inputValues[key] ?? inputSubSchema.default}
|
||||
placeholder={inputSubSchema.description}
|
||||
onChange={(value) =>
|
||||
setInputValues((obj) => ({
|
||||
...obj,
|
||||
[key]: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Pop-out Long Input Modal */}
|
||||
<Dialog
|
||||
open={expandedInputKey !== null}
|
||||
onOpenChange={(open) => !open && closeInputPopout()}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[720px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Edit {expandedInputKey && agentInputs[expandedInputKey].title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
value={tempInputValue}
|
||||
onChange={(e) => setTempInputValue(e.target.value)}
|
||||
className="min-h-[320px]"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={closeInputPopout}>Cancel</Button>
|
||||
<Button variant="primary" onClick={saveAndCloseInputPopout}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Actions */}
|
||||
<aside className="w-48 xl:w-56">
|
||||
<div className="flex flex-col gap-8">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// history.ts
|
||||
import { CustomNodeData } from "./CustomNode";
|
||||
import { CustomEdgeData } from "./CustomEdge";
|
||||
import { Edge } from "@xyflow/react";
|
||||
|
||||
@@ -49,27 +49,8 @@ export const FlowInfo: React.FC<
|
||||
refresh: () => void;
|
||||
}
|
||||
> = ({ flow, executions, flowVersion, refresh, ...props }) => {
|
||||
const {
|
||||
agentName,
|
||||
setAgentName,
|
||||
agentDescription,
|
||||
setAgentDescription,
|
||||
savedAgent,
|
||||
availableNodes,
|
||||
availableFlows,
|
||||
getOutputType,
|
||||
requestSave,
|
||||
requestSaveAndRun,
|
||||
requestStopRun,
|
||||
scheduleRunner,
|
||||
isRunning,
|
||||
isScheduling,
|
||||
setIsScheduling,
|
||||
nodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = useAgentGraph(flow.agent_id, flow.agent_version, undefined, false);
|
||||
const { requestSaveAndRun, requestStopRun, isRunning, nodes, setNodes } =
|
||||
useAgentGraph(flow.agent_id, flow.agent_version, undefined, false);
|
||||
|
||||
const api = useBackendAPI();
|
||||
const { toast } = useToast();
|
||||
@@ -85,7 +66,6 @@ export const FlowInfo: React.FC<
|
||||
);
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [openCron, setOpenCron] = useState(false);
|
||||
const [isRunnerInputOpen, setIsRunnerInputOpen] = useState(false);
|
||||
const isDisabled = !selectedFlowVersion;
|
||||
|
||||
@@ -110,9 +90,6 @@ export const FlowInfo: React.FC<
|
||||
value: (node.data.hardcodedValues as any).value,
|
||||
placeholder_values:
|
||||
(node.data.hardcodedValues as any).placeholder_values || [],
|
||||
limit_to_placeholder_values:
|
||||
(node.data.hardcodedValues as any).limit_to_placeholder_values ||
|
||||
false,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -132,17 +109,6 @@ export const FlowInfo: React.FC<
|
||||
return { inputs, outputs };
|
||||
}, [nodes]);
|
||||
|
||||
const handleScheduleButton = () => {
|
||||
if (!selectedFlowVersion) {
|
||||
toast({
|
||||
title: "Please select a flow version before scheduling",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setOpenCron(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.getGraphAllVersions(flow.agent_id)
|
||||
@@ -161,7 +127,7 @@ export const FlowInfo: React.FC<
|
||||
};
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(nodeId: string, field: string, value: string) => {
|
||||
(nodeId: string, field: string, value: any) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
|
||||
@@ -18,8 +18,17 @@ import {
|
||||
BlockIONumberSubSchema,
|
||||
BlockIOBooleanSubSchema,
|
||||
BlockIOSimpleTypeSubSchema,
|
||||
DataType,
|
||||
determineDataType,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Switch } from "./ui/switch";
|
||||
import {
|
||||
@@ -102,92 +111,6 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
|
||||
|
||||
export default NodeObjectInputTree;
|
||||
|
||||
const NodeImageInput: FC<{
|
||||
selfKey: string;
|
||||
schema: BlockIOStringSubSchema;
|
||||
value?: string;
|
||||
error?: string;
|
||||
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
|
||||
className?: string;
|
||||
displayName: string;
|
||||
}> = ({
|
||||
selfKey,
|
||||
schema,
|
||||
value = "",
|
||||
error,
|
||||
handleInputChange,
|
||||
className,
|
||||
displayName,
|
||||
}) => {
|
||||
const handleFileChange = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith("image/")) {
|
||||
console.error("Please upload an image file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to base64
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = (e.target?.result as string).split(",")[1];
|
||||
handleInputChange(selfKey, base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
[selfKey, handleInputChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
<div className="nodrag flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
document.getElementById(`${selfKey}-upload`)?.click()
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{value ? "Change Image" : `Upload ${displayName}`}
|
||||
</Button>
|
||||
{value && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleInputChange(selfKey, "")}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
id={`${selfKey}-upload`}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{value && (
|
||||
<div className="relative mt-2 rounded-md border border-gray-300 p-2 dark:border-gray-600">
|
||||
<img
|
||||
src={`data:image/jpeg;base64,${value}`}
|
||||
alt="Preview"
|
||||
className="max-h-32 w-full rounded-md object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeDateTimeInput: FC<{
|
||||
selfKey: string;
|
||||
schema: BlockIOStringSubSchema;
|
||||
@@ -209,10 +132,8 @@ const NodeDateTimeInput: FC<{
|
||||
hideDate = false,
|
||||
hideTime = false,
|
||||
}) => {
|
||||
const date = value ? new Date(value) : undefined;
|
||||
const [timeInput, setTimeInput] = useState(
|
||||
value && date ? format(date, "HH:mm") : "00:00",
|
||||
);
|
||||
const dateInput = value && !hideDate ? new Date(value) : new Date();
|
||||
const timeInput = value && !hideTime ? format(dateInput, "HH:mm") : "00:00";
|
||||
|
||||
const handleDateSelect = (newDate: Date | undefined) => {
|
||||
if (!newDate) return;
|
||||
@@ -230,9 +151,6 @@ const NodeDateTimeInput: FC<{
|
||||
|
||||
const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newTime = e.target.value;
|
||||
setTimeInput(newTime);
|
||||
|
||||
if (!value) return;
|
||||
|
||||
if (hideDate) {
|
||||
// Only pass HH:mm if date is hidden
|
||||
@@ -240,9 +158,8 @@ const NodeDateTimeInput: FC<{
|
||||
} else {
|
||||
// Otherwise pass full date/time
|
||||
const [hours, minutes] = newTime.split(":").map(Number);
|
||||
const newDate = new Date(value);
|
||||
newDate.setHours(hours, minutes);
|
||||
handleInputChange(selfKey, newDate.toISOString());
|
||||
dateInput.setHours(hours, minutes);
|
||||
handleInputChange(selfKey, dateInput.toISOString());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,13 +176,17 @@ const NodeDateTimeInput: FC<{
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{value && date ? format(date, "PPP") : <span>Pick a date</span>}
|
||||
{value && dateInput ? (
|
||||
format(dateInput, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
selected={dateInput}
|
||||
onSelect={handleDateSelect}
|
||||
autoFocus
|
||||
/>
|
||||
@@ -305,6 +226,7 @@ const NodeFileInput: FC<{
|
||||
const handleFileChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
console.log(">>> file", file);
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
@@ -337,15 +259,15 @@ const NodeFileInput: FC<{
|
||||
return "File";
|
||||
}, []);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
<div className="nodrag flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
document.getElementById(`${selfKey}-upload`)?.click()
|
||||
}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="w-full"
|
||||
>
|
||||
{value ? `Change ${displayName}` : `Upload ${displayName}`}
|
||||
@@ -354,14 +276,17 @@ const NodeFileInput: FC<{
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleInputChange(selfKey, "")}
|
||||
onClick={() => {
|
||||
inputRef.current && (inputRef.current!.value = "");
|
||||
handleInputChange(selfKey, "");
|
||||
}}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
id={`${selfKey}-upload`}
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="*/*"
|
||||
onChange={handleFileChange}
|
||||
@@ -404,283 +329,8 @@ export const NodeGenericInputField: FC<{
|
||||
className = cn(className);
|
||||
displayName ||= propSchema.title || beautifyString(propKey);
|
||||
|
||||
if ("allOf" in propSchema) {
|
||||
// If this happens, that is because Pydantic wraps $refs in an allOf if the
|
||||
// $ref has sibling schema properties (which isn't technically allowed),
|
||||
// so there will only be one item in allOf[].
|
||||
// AFAIK this should NEVER happen though, as $refs are resolved server-side.
|
||||
propSchema = propSchema.allOf[0];
|
||||
console.warn(`Unsupported 'allOf' in schema for '${propKey}'!`, propSchema);
|
||||
}
|
||||
|
||||
if ("credentials_provider" in propSchema) {
|
||||
return (
|
||||
<NodeCredentialsInput
|
||||
selfKey={propKey}
|
||||
value={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ("properties" in propSchema) {
|
||||
// Render a multi-select for all-boolean sub-schemas with more than 3 properties
|
||||
if (
|
||||
Object.values(propSchema.properties).every(
|
||||
(subSchema) => "type" in subSchema && subSchema.type == "boolean",
|
||||
) &&
|
||||
Object.keys(propSchema.properties).length >= 3
|
||||
) {
|
||||
const options = Object.keys(propSchema.properties);
|
||||
const selectedKeys = Object.entries(currentValue || {})
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, _]) => k);
|
||||
return (
|
||||
<NodeMultiSelectInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
selection={selectedKeys}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={(key, selection) => {
|
||||
handleInputChange(
|
||||
key,
|
||||
Object.fromEntries(
|
||||
options.map((option) => [option, selection.includes(option)]),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeObjectInputTree
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
object={currentValue}
|
||||
errors={errors}
|
||||
className={cn("border-l border-gray-500 pl-2", className)} // visual indent
|
||||
displayName={displayName}
|
||||
connections={connections}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ("additionalProperties" in propSchema) {
|
||||
return (
|
||||
<NodeKeyValueInput
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
entries={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
connections={connections}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ("anyOf" in propSchema) {
|
||||
// Optional oneOf
|
||||
if (
|
||||
"oneOf" in propSchema.anyOf[0] &&
|
||||
propSchema.anyOf[0].oneOf &&
|
||||
"discriminator" in propSchema.anyOf[0] &&
|
||||
propSchema.anyOf[0].discriminator
|
||||
) {
|
||||
return (
|
||||
<NodeOneOfDiscriminatorField
|
||||
nodeId={nodeId}
|
||||
propKey={propKey}
|
||||
propSchema={propSchema.anyOf[0]}
|
||||
defaultValue={propSchema.default}
|
||||
currentValue={currentValue}
|
||||
errors={errors}
|
||||
connections={connections}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// optional items
|
||||
const types = propSchema.anyOf.map((s) =>
|
||||
"type" in s ? s.type : undefined,
|
||||
);
|
||||
if (types.includes("string") && types.includes("null")) {
|
||||
// optional string and datetime
|
||||
|
||||
if (
|
||||
"format" in propSchema.anyOf[0] &&
|
||||
["date-time", "date", "time"].includes(
|
||||
propSchema.anyOf[0].format as string,
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<NodeDateTimeInput
|
||||
hideDate={propSchema.anyOf[0].format === "time"}
|
||||
hideTime={propSchema.anyOf[0].format === "date"}
|
||||
selfKey={propKey}
|
||||
schema={propSchema.anyOf[0]}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
"format" in propSchema.anyOf[0] &&
|
||||
propSchema.anyOf[0].format === "file"
|
||||
) {
|
||||
return (
|
||||
<NodeFileInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema.anyOf[0] as BlockIOStringSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
handleInputChange={handleInputChange}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeStringInput
|
||||
selfKey={propKey}
|
||||
schema={
|
||||
{
|
||||
...propSchema,
|
||||
type: "string",
|
||||
enum: (propSchema.anyOf[0] as BlockIOStringSubSchema).enum,
|
||||
} as BlockIOStringSubSchema
|
||||
}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
(types.includes("integer") || types.includes("number")) &&
|
||||
types.includes("null")
|
||||
) {
|
||||
return (
|
||||
<NodeNumberInput
|
||||
selfKey={propKey}
|
||||
schema={
|
||||
{
|
||||
...propSchema,
|
||||
type: "integer",
|
||||
} as BlockIONumberSubSchema
|
||||
}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
} else if (types.includes("array") && types.includes("null")) {
|
||||
return (
|
||||
<NodeArrayInput
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={
|
||||
{
|
||||
...propSchema,
|
||||
type: "array",
|
||||
items: (propSchema.anyOf[0] as BlockIOArraySubSchema).items,
|
||||
} as BlockIOArraySubSchema
|
||||
}
|
||||
entries={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
connections={connections}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
);
|
||||
} else if (types.includes("object") && types.includes("null")) {
|
||||
// rendering optional mutliselect
|
||||
if (
|
||||
Object.values(
|
||||
(propSchema.anyOf[0] as BlockIOObjectSubSchema).properties,
|
||||
).every(
|
||||
(subSchema) => "type" in subSchema && subSchema.type == "boolean",
|
||||
) &&
|
||||
Object.keys((propSchema.anyOf[0] as BlockIOObjectSubSchema).properties)
|
||||
.length >= 1
|
||||
) {
|
||||
const options = Object.keys(
|
||||
(propSchema.anyOf[0] as BlockIOObjectSubSchema).properties,
|
||||
);
|
||||
const selectedKeys = Object.entries(currentValue || {})
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, _]) => k);
|
||||
return (
|
||||
<NodeMultiSelectInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema.anyOf[0] as BlockIOObjectSubSchema}
|
||||
selection={selectedKeys}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={(key, selection) => {
|
||||
handleInputChange(
|
||||
key,
|
||||
Object.fromEntries(
|
||||
options.map((option) => [option, selection.includes(option)]),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeKeyValueInput
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={
|
||||
{
|
||||
...propSchema,
|
||||
type: "object",
|
||||
additionalProperties: (propSchema.anyOf[0] as BlockIOKVSubSchema)
|
||||
.additionalProperties,
|
||||
} as BlockIOKVSubSchema
|
||||
}
|
||||
entries={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
connections={connections}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
"oneOf" in propSchema &&
|
||||
propSchema.oneOf &&
|
||||
"discriminator" in propSchema &&
|
||||
propSchema.discriminator
|
||||
) {
|
||||
@@ -701,71 +351,56 @@ export const NodeGenericInputField: FC<{
|
||||
);
|
||||
}
|
||||
|
||||
if (!("type" in propSchema)) {
|
||||
return (
|
||||
<NodeFallbackInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const dt = determineDataType(propSchema);
|
||||
switch (dt) {
|
||||
case DataType.CREDENTIALS:
|
||||
return (
|
||||
<NodeCredentialsInput
|
||||
selfKey={propKey}
|
||||
value={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
|
||||
switch (propSchema.type) {
|
||||
case "string":
|
||||
if ("image_upload" in propSchema && propSchema.image_upload === true) {
|
||||
return (
|
||||
<NodeImageInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (
|
||||
"format" in propSchema &&
|
||||
["date-time", "date", "time"].includes(propSchema.format as string)
|
||||
) {
|
||||
return (
|
||||
<NodeDateTimeInput
|
||||
hideDate={propSchema.format === "time"}
|
||||
hideTime={propSchema.format === "date"}
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if ("format" in propSchema && propSchema.format === "file") {
|
||||
return (
|
||||
<NodeFileInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
handleInputChange={handleInputChange}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case DataType.DATE:
|
||||
case DataType.TIME:
|
||||
case DataType.DATE_TIME:
|
||||
const hideDate = dt === DataType.TIME;
|
||||
const hideTime = dt === DataType.DATE;
|
||||
return (
|
||||
<NodeDateTimeInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema as BlockIOStringSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
hideDate={hideDate}
|
||||
hideTime={hideTime}
|
||||
/>
|
||||
);
|
||||
|
||||
case DataType.FILE:
|
||||
return (
|
||||
<NodeFileInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema as BlockIOStringSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
handleInputChange={handleInputChange}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
/>
|
||||
);
|
||||
|
||||
case DataType.SELECT:
|
||||
return (
|
||||
<NodeStringInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
schema={propSchema as BlockIOStringSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
@@ -774,11 +409,41 @@ export const NodeGenericInputField: FC<{
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
);
|
||||
case "boolean":
|
||||
|
||||
case DataType.MULTI_SELECT:
|
||||
const schema = propSchema as BlockIOObjectSubSchema;
|
||||
return (
|
||||
<NodeMultiSelectInput
|
||||
selfKey={propKey}
|
||||
schema={schema}
|
||||
selection={Object.entries(currentValue || {})
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, _]) => k)}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={(key, selection) => {
|
||||
// If you want to build an object of booleans from `selection`
|
||||
// (like your old code), do it here. Otherwise adapt to your actual UI.
|
||||
// Example:
|
||||
const allKeys = schema.properties
|
||||
? Object.keys(schema.properties)
|
||||
: [];
|
||||
handleInputChange(
|
||||
key,
|
||||
Object.fromEntries(
|
||||
allKeys.map((opt) => [opt, selection.includes(opt)]),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case DataType.BOOLEAN:
|
||||
return (
|
||||
<NodeBooleanInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
schema={propSchema as BlockIOBooleanSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
@@ -786,12 +451,12 @@ export const NodeGenericInputField: FC<{
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
case "number":
|
||||
case "integer":
|
||||
|
||||
case DataType.NUMBER:
|
||||
return (
|
||||
<NodeNumberInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
schema={propSchema as BlockIONumberSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
@@ -799,12 +464,13 @@ export const NodeGenericInputField: FC<{
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
case "array":
|
||||
|
||||
case DataType.ARRAY:
|
||||
return (
|
||||
<NodeArrayInput
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
schema={propSchema as BlockIOArraySubSchema}
|
||||
entries={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
@@ -814,12 +480,13 @@ export const NodeGenericInputField: FC<{
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
);
|
||||
case "object":
|
||||
|
||||
case DataType.KEY_VALUE:
|
||||
return (
|
||||
<NodeKeyValueInput
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
schema={propSchema as BlockIOKVSubSchema}
|
||||
entries={currentValue}
|
||||
errors={errors}
|
||||
className={className}
|
||||
@@ -828,15 +495,30 @@ export const NodeGenericInputField: FC<{
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
console.warn(
|
||||
`Schema for '${propKey}' specifies unknown type:`,
|
||||
propSchema,
|
||||
);
|
||||
|
||||
case DataType.OBJECT:
|
||||
return (
|
||||
<NodeFallbackInput
|
||||
<NodeObjectInputTree
|
||||
nodeId={nodeId}
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
schema={propSchema as any}
|
||||
object={currentValue}
|
||||
errors={errors}
|
||||
className={cn("border-l border-gray-500 pl-2", className)} // visual indent
|
||||
displayName={displayName}
|
||||
connections={connections}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
|
||||
case DataType.LONG_TEXT:
|
||||
case DataType.SHORT_TEXT:
|
||||
default:
|
||||
return (
|
||||
<NodeStringInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema as BlockIOStringSubSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
@@ -1036,10 +718,6 @@ const NodeCredentialsInput: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const InputRef = (value: any): ((el: HTMLInputElement | null) => void) => {
|
||||
return (el) => el && value != null && (el.value = value);
|
||||
};
|
||||
|
||||
const NodeKeyValueInput: FC<{
|
||||
nodeId: string;
|
||||
selfKey: string;
|
||||
@@ -1123,6 +801,8 @@ const NodeKeyValueInput: FC<{
|
||||
// because the `key` can change with each input, causing the input to lose focus.
|
||||
<div key={index}>
|
||||
<NodeHandle
|
||||
title={`#${key}`}
|
||||
className="text-sm text-gray-500"
|
||||
keyName={getEntryKey(key)}
|
||||
schema={{ type: "string" }}
|
||||
isConnected={isConnected(key)}
|
||||
@@ -1254,6 +934,8 @@ const NodeArrayInput: FC<{
|
||||
return (
|
||||
<div key={entryKey}>
|
||||
<NodeHandle
|
||||
title={`#${index + 1}`}
|
||||
className="text-sm text-gray-500"
|
||||
keyName={entryKey}
|
||||
schema={schema.items!}
|
||||
isConnected={isConnected}
|
||||
@@ -1271,6 +953,7 @@ const NodeArrayInput: FC<{
|
||||
currentValue={entry}
|
||||
errors={errors}
|
||||
connections={connections}
|
||||
displayName={displayName || beautifyString(selfKey)}
|
||||
handleInputChange={handleInputChange}
|
||||
handleInputClick={handleInputClick}
|
||||
/>
|
||||
@@ -1388,7 +1071,7 @@ const NodeStringInput: FC<{
|
||||
value ||= schema.default || "";
|
||||
return (
|
||||
<div className={className}>
|
||||
{schema.enum ? (
|
||||
{schema.enum && schema.enum.length > 0 ? (
|
||||
<Select
|
||||
defaultValue={value}
|
||||
onValueChange={(newValue) => handleInputChange(selfKey, newValue)}
|
||||
@@ -1397,11 +1080,13 @@ const NodeStringInput: FC<{
|
||||
<SelectValue placeholder={schema.placeholder || displayName} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="nodrag">
|
||||
{schema.enum.map((option, index) => (
|
||||
<SelectItem key={index} value={option}>
|
||||
{beautifyString(option)}
|
||||
</SelectItem>
|
||||
))}
|
||||
{schema.enum
|
||||
.filter((option) => option)
|
||||
.map((option, index) => (
|
||||
<SelectItem key={index} value={option}>
|
||||
{beautifyString(option)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
BlockIOStringSubSchema,
|
||||
BlockIOSubSchema,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { TypeBasedInput } from "@/components/type-based-input";
|
||||
import SchemaTooltip from "../SchemaTooltip";
|
||||
|
||||
interface InputBlockProps {
|
||||
id: string;
|
||||
name: string;
|
||||
schema: BlockIOSubSchema;
|
||||
description?: string;
|
||||
value: string;
|
||||
placeholder_values?: any[];
|
||||
@@ -20,47 +20,30 @@ interface InputBlockProps {
|
||||
export function InputBlock({
|
||||
id,
|
||||
name,
|
||||
schema,
|
||||
description,
|
||||
value,
|
||||
placeholder_values,
|
||||
onInputChange,
|
||||
}: InputBlockProps) {
|
||||
if (placeholder_values && placeholder_values.length > 0) {
|
||||
schema = { ...schema, enum: placeholder_values } as BlockIOStringSubSchema;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold">{name || "Unnamed Input"}</h3>
|
||||
{description && <p className="text-sm text-gray-600">{description}</p>}
|
||||
<div>
|
||||
{placeholder_values && placeholder_values.length > 1 ? (
|
||||
<Select
|
||||
onValueChange={(value) => onInputChange(id, "value", value)}
|
||||
value={value}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a value" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{placeholder_values.map((placeholder, index) => (
|
||||
<SelectItem
|
||||
key={index}
|
||||
value={placeholder.toString()}
|
||||
data-testid={`run-dialog-input-${name}-${placeholder.toString()}`}
|
||||
>
|
||||
{placeholder.toString()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={`${id}-Value`}
|
||||
data-testid={`run-dialog-input-${name}`}
|
||||
value={value}
|
||||
onChange={(e) => onInputChange(id, "value", e.target.value)}
|
||||
placeholder={placeholder_values?.[0]?.toString() || "Enter value"}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-1 text-sm font-medium">
|
||||
{name || "Unnamed Input"}
|
||||
<SchemaTooltip description={description} />
|
||||
</label>
|
||||
<TypeBasedInput
|
||||
id={`${id}-Value`}
|
||||
data-testid={`run-dialog-input-${name}`}
|
||||
schema={schema}
|
||||
value={value}
|
||||
placeholder={description}
|
||||
onChange={(value) => onInputChange(id, "value", value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,21 +5,22 @@ import { BlockInput } from "./RunnerInputUI";
|
||||
|
||||
interface InputListProps {
|
||||
blockInputs: BlockInput[];
|
||||
onInputChange: (nodeId: string, field: string, value: string) => void;
|
||||
onInputChange: (nodeId: string, field: string, value: any) => void;
|
||||
}
|
||||
|
||||
export function InputList({ blockInputs, onInputChange }: InputListProps) {
|
||||
return (
|
||||
<ScrollArea className="h-[20vh] overflow-auto pr-4 sm:h-[30vh] md:h-[40vh] lg:h-[50vh]">
|
||||
<ScrollArea className="max-h-[60vh] overflow-auto">
|
||||
<div className="space-y-4">
|
||||
{blockInputs && blockInputs.length > 0 ? (
|
||||
blockInputs.map((block) => (
|
||||
<InputBlock
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
schema={block.inputSchema}
|
||||
name={block.hardcodedValues.name}
|
||||
description={block.hardcodedValues.description}
|
||||
value={block.hardcodedValues.value?.toString() || ""}
|
||||
value={block.hardcodedValues.value || ""}
|
||||
placeholder_values={block.hardcodedValues.placeholder_values}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface BlockInput {
|
||||
description: string;
|
||||
value: any;
|
||||
placeholder_values?: any[];
|
||||
limit_to_placeholder_values?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +26,7 @@ interface RunSettingsUiProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
blockInputs: BlockInput[];
|
||||
onInputChange: (nodeId: string, field: string, value: string) => void;
|
||||
onInputChange: (nodeId: string, field: string, value: any) => void;
|
||||
onRun: () => void;
|
||||
onSchedule: () => Promise<void>;
|
||||
scheduledInput: boolean;
|
||||
@@ -58,8 +57,8 @@ export function RunnerInputUI({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="flex max-h-[80vh] flex-col overflow-hidden sm:max-w-[400px] md:max-w-[500px] lg:max-w-[600px]">
|
||||
<DialogHeader className="px-4 py-4">
|
||||
<DialogContent className="flex flex-col px-10 py-8">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
{scheduledInput ? "Schedule Settings" : "Run Settings"}
|
||||
</DialogTitle>
|
||||
@@ -67,14 +66,14 @@ export function RunnerInputUI({
|
||||
Configure settings for running your agent.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-grow overflow-y-auto px-4 py-4">
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<InputList blockInputs={blockInputs} onInputChange={onInputChange} />
|
||||
</div>
|
||||
<DialogFooter className="px-6 py-4">
|
||||
<DialogFooter>
|
||||
<Button
|
||||
data-testid="run-dialog-run-button"
|
||||
onClick={scheduledInput ? handleSchedule : handleRun}
|
||||
className="px-8 py-2 text-lg"
|
||||
className="text-lg"
|
||||
disabled={scheduledInput ? isScheduling : isRunning}
|
||||
>
|
||||
{scheduledInput ? "Schedule" : isRunning ? "Running..." : "Run"}
|
||||
|
||||
425
autogpt_platform/frontend/src/components/type-based-input.tsx
Normal file
425
autogpt_platform/frontend/src/components/type-based-input.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
import React, { FC } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { Cross2Icon, FileTextIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import { Input as BaseInput } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@/components/ui/select";
|
||||
import { determineDataType, DataType } from "@/lib/autogpt-server-api/types";
|
||||
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
/**
|
||||
* A generic prop structure for the TypeBasedInput.
|
||||
*
|
||||
* onChange expects an event-like object with e.target.value so the parent
|
||||
* can do something like setInputValues(e.target.value).
|
||||
*/
|
||||
export interface TypeBasedInputProps {
|
||||
schema: BlockIOSubSchema;
|
||||
value?: any;
|
||||
placeholder?: string;
|
||||
onChange: (value: any) => void;
|
||||
}
|
||||
|
||||
const inputClasses = "min-h-11 rounded-full border px-4 py-2.5";
|
||||
|
||||
function Input({
|
||||
className,
|
||||
...props
|
||||
}: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
return <BaseInput {...props} className={cn(inputClasses, className)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic, data-type-based input component that uses Shadcn UI.
|
||||
* It inspects the schema via `determineDataType` and renders
|
||||
* the correct UI component.
|
||||
*/
|
||||
export const TypeBasedInput: FC<
|
||||
TypeBasedInputProps & React.HTMLAttributes<HTMLElement>
|
||||
> = ({ schema, value, placeholder, onChange, ...props }) => {
|
||||
const dataType = determineDataType(schema);
|
||||
|
||||
let innerInputElement: React.ReactNode = null;
|
||||
switch (dataType) {
|
||||
case DataType.NUMBER:
|
||||
innerInputElement = (
|
||||
<Input
|
||||
type="number"
|
||||
value={value ?? ""}
|
||||
placeholder={placeholder || "Enter number"}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.LONG_TEXT:
|
||||
innerInputElement = (
|
||||
<Textarea
|
||||
className="rounded-[12px] px-3 py-2"
|
||||
value={value ?? ""}
|
||||
placeholder={placeholder || "Enter text"}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.BOOLEAN:
|
||||
innerInputElement = (
|
||||
<>
|
||||
<span className="text-sm text-gray-500">{placeholder}</span>
|
||||
<Switch
|
||||
className={placeholder ? "ml-auto" : "mx-auto"}
|
||||
checked={!!value}
|
||||
onCheckedChange={(checked) => onChange(checked)}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.DATE:
|
||||
innerInputElement = (
|
||||
<DatePicker
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
className={cn(inputClasses)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.TIME:
|
||||
innerInputElement = (
|
||||
<TimePicker value={value?.toString()} onChange={onChange} />
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.DATE_TIME:
|
||||
innerInputElement = (
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder || "Enter date and time"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.FILE:
|
||||
innerInputElement = (
|
||||
<FileInput
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case DataType.SELECT:
|
||||
if (
|
||||
"enum" in schema &&
|
||||
Array.isArray(schema.enum) &&
|
||||
schema.enum.length > 0
|
||||
) {
|
||||
innerInputElement = (
|
||||
<Select value={value ?? ""} onValueChange={(val) => onChange(val)}>
|
||||
<SelectTrigger
|
||||
className={cn(inputClasses, "text-sm text-gray-500")}
|
||||
>
|
||||
<SelectValue placeholder={placeholder || "Select an option"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-[12px] border">
|
||||
{schema.enum
|
||||
.filter((opt) => opt)
|
||||
.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{String(opt)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.SHORT_TEXT:
|
||||
default:
|
||||
innerInputElement = (
|
||||
<Input
|
||||
type="text"
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder || "Enter text"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="no-drag relative flex">{innerInputElement}</div>;
|
||||
};
|
||||
|
||||
interface DatePickerProps {
|
||||
value?: Date;
|
||||
placeholder?: string;
|
||||
onChange: (date: Date | undefined) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DatePicker({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
className,
|
||||
}: DatePickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-start font-normal",
|
||||
!value && "text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-5 w-5" />
|
||||
{value ? (
|
||||
format(value, "PPP")
|
||||
) : (
|
||||
<span>{placeholder || "Pick a date"}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="flex min-h-[340px] w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={(selected) => onChange(selected)}
|
||||
autoFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimePickerProps {
|
||||
value?: string;
|
||||
onChange: (time: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimePicker({ value, onChange }: TimePickerProps) {
|
||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||
const [hourNum, minuteNum] = value ? value.split(":").map(Number) : [0, 0];
|
||||
|
||||
const meridiem = hourNum >= 12 ? "PM" : "AM";
|
||||
const hour = pad(hourNum % 12 || 12);
|
||||
const minute = pad(minuteNum);
|
||||
|
||||
const changeTime = (hour: string, minute: string, meridiem: string) => {
|
||||
const hour24 = (Number(hour) % 12) + (meridiem === "PM" ? 12 : 0);
|
||||
onChange(`${pad(hour24)}:${minute}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<Select
|
||||
value={hour}
|
||||
onValueChange={(val) => changeTime(val, minute, meridiem)}
|
||||
>
|
||||
<SelectTrigger className={cn("text-center", inputClasses)}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: 12 }, (_, i) => pad(i + 1)).map((h) => (
|
||||
<SelectItem key={h} value={h}>
|
||||
{h}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="m-auto text-xl font-bold">:</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<Select
|
||||
value={minute}
|
||||
onValueChange={(val) => changeTime(hour, val, meridiem)}
|
||||
>
|
||||
<SelectTrigger className={cn("text-center", inputClasses)}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: 60 }, (_, i) => pad(i)).map((m) => (
|
||||
<SelectItem key={m} value={m.toString()}>
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<Select
|
||||
value={meridiem}
|
||||
onValueChange={(val) => changeTime(hour, minute, val)}
|
||||
>
|
||||
<SelectTrigger className={cn("text-center", inputClasses)}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AM">AM</SelectItem>
|
||||
<SelectItem value="PM">PM</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getFileLabel(value: string) {
|
||||
if (value.startsWith("data:")) {
|
||||
const matches = value.match(/^data:([^;]+);/);
|
||||
if (matches?.[1]) {
|
||||
const mimeParts = matches[1].split("/");
|
||||
if (mimeParts.length > 1) {
|
||||
return `${mimeParts[1].toUpperCase()} file`;
|
||||
}
|
||||
return `${matches[1]} file`;
|
||||
}
|
||||
} else {
|
||||
const pathParts = value.split(".");
|
||||
if (pathParts.length > 1) {
|
||||
const ext = pathParts.pop();
|
||||
if (ext) return `${ext.toUpperCase()} file`;
|
||||
}
|
||||
}
|
||||
return "File";
|
||||
}
|
||||
|
||||
function getFileSize(value: string) {
|
||||
if (value.startsWith("data:")) {
|
||||
const matches = value.match(/;base64,(.*)/);
|
||||
if (matches?.[1]) {
|
||||
const size = Math.ceil((matches[1].length * 3) / 4);
|
||||
if (size > 1024 * 1024) {
|
||||
return `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
||||
} else {
|
||||
return `${(size / 1024).toFixed(2)} KB`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
interface FileInputProps {
|
||||
value?: string; // base64 string or empty
|
||||
placeholder?: string; // e.g. "Resume", "Document", etc.
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FileInput: FC<FileInputProps> = ({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const loadFile = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target?.result as string;
|
||||
onChange(base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) loadFile(file);
|
||||
};
|
||||
|
||||
const handleFileDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file) loadFile(file);
|
||||
};
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
{value ? (
|
||||
<div className="flex min-h-14 items-center gap-4">
|
||||
<div className="agpt-border-input flex min-h-14 w-full items-center justify-between rounded-[12px] bg-zinc-50 p-4 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileTextIcon className="h-7 w-7 text-black" />
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-normal text-black">
|
||||
{getFileLabel(value)}
|
||||
</span>
|
||||
<span>{getFileSize(value)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Cross2Icon
|
||||
className="h-5 w-5 cursor-pointer text-black"
|
||||
onClick={() => {
|
||||
inputRef.current && (inputRef.current.value = "");
|
||||
onChange("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-14 items-center gap-4">
|
||||
<div
|
||||
onDrop={handleFileDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
className="agpt-border-input flex min-h-14 w-full items-center justify-center rounded-[12px] border-dashed bg-zinc-50 text-sm text-gray-500"
|
||||
>
|
||||
Choose a file or drag and drop it here
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={() => inputRef.current?.click()}>
|
||||
Browse File
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="*/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -42,8 +42,8 @@ function Calendar({
|
||||
caption_label: "hidden",
|
||||
nav: "hidden",
|
||||
button_previous:
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 ",
|
||||
button_next: " h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
button_next: "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
month_grid: "w-full border-collapse space-y-1",
|
||||
weekdays: "flex",
|
||||
weekday:
|
||||
|
||||
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"agpt-border-input flex min-h-[80px] w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"agpt-border-input flex min-h-[80px] w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -153,10 +153,11 @@ export default function useAgentGraph(
|
||||
setAgentDescription(graph.description);
|
||||
|
||||
setNodes((prevNodes) => {
|
||||
const newNodes = graph.nodes.map((node) => {
|
||||
const _newNodes = graph.nodes.map((node) => {
|
||||
const block = availableNodes.find(
|
||||
(block) => block.id === node.block_id,
|
||||
)!;
|
||||
if (!block) return null;
|
||||
const prevNode = prevNodes.find((n) => n.id === node.id);
|
||||
const flow =
|
||||
block.uiType == BlockUIType.AGENT
|
||||
@@ -199,6 +200,7 @@ export default function useAgentGraph(
|
||||
};
|
||||
return newNode;
|
||||
});
|
||||
const newNodes = _newNodes.filter((n) => n !== null);
|
||||
setEdges(() =>
|
||||
graph.links.map((link) => {
|
||||
const adjustedSourceName = link.source_name?.startsWith("tools_^_")
|
||||
@@ -411,7 +413,10 @@ export default function useAgentGraph(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.warn("Error", error);
|
||||
console.warn(`Error in ${node.data.blockType}: ${error}`, {
|
||||
data: inputData,
|
||||
schema: node.data.inputSchema,
|
||||
});
|
||||
errorMessage = error.message || "Invalid input";
|
||||
if (path && error.message) {
|
||||
const key = path.slice(1);
|
||||
|
||||
@@ -65,6 +65,23 @@ export type BlockIOSimpleTypeSubSchema =
|
||||
| BlockIOBooleanSubSchema
|
||||
| BlockIONullSubSchema;
|
||||
|
||||
export enum DataType {
|
||||
SHORT_TEXT = "short-text",
|
||||
LONG_TEXT = "long-text",
|
||||
NUMBER = "number",
|
||||
DATE = "date",
|
||||
TIME = "time",
|
||||
DATE_TIME = "date-time",
|
||||
FILE = "file",
|
||||
SELECT = "select",
|
||||
MULTI_SELECT = "multi-select",
|
||||
BOOLEAN = "boolean",
|
||||
CREDENTIALS = "credentials",
|
||||
OBJECT = "object",
|
||||
KEY_VALUE = "key-value",
|
||||
ARRAY = "array",
|
||||
}
|
||||
|
||||
export type BlockIOSubSchemaMeta = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@@ -102,6 +119,7 @@ export type BlockIOStringSubSchema = BlockIOSubSchemaMeta & {
|
||||
secret?: true;
|
||||
default?: string;
|
||||
format?: string;
|
||||
maxLength?: number;
|
||||
};
|
||||
|
||||
export type BlockIONumberSubSchema = BlockIOSubSchemaMeta & {
|
||||
@@ -276,6 +294,7 @@ export type GraphIOSubSchema = Omit<
|
||||
type: never; // bodge to avoid type checking hell; doesn't exist at runtime
|
||||
default?: string;
|
||||
secret: boolean;
|
||||
metadata?: any;
|
||||
};
|
||||
|
||||
/* Mirror of backend/data/graph.py:Graph */
|
||||
@@ -845,3 +864,142 @@ export type AdminPendingSubmissionsRequest = {
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
const _stringFormatToDataTypeMap: Partial<Record<string, DataType>> = {
|
||||
date: DataType.DATE,
|
||||
time: DataType.TIME,
|
||||
file: DataType.FILE,
|
||||
"date-time": DataType.DATE_TIME,
|
||||
"short-text": DataType.SHORT_TEXT,
|
||||
"long-text": DataType.LONG_TEXT,
|
||||
};
|
||||
|
||||
function _handleStringSchema(strSchema: BlockIOStringSubSchema): DataType {
|
||||
if (strSchema.format) {
|
||||
const type = _stringFormatToDataTypeMap[strSchema.format];
|
||||
if (type) return type;
|
||||
}
|
||||
if (strSchema.enum) return DataType.SELECT;
|
||||
if (strSchema.maxLength && strSchema.maxLength > 200)
|
||||
return DataType.LONG_TEXT;
|
||||
return DataType.SHORT_TEXT;
|
||||
}
|
||||
|
||||
function _handleSingleTypeSchema(subSchema: BlockIOSubSchema): DataType {
|
||||
if (subSchema.type === "string") {
|
||||
return _handleStringSchema(subSchema as BlockIOStringSubSchema);
|
||||
}
|
||||
if (subSchema.type === "boolean") {
|
||||
return DataType.BOOLEAN;
|
||||
}
|
||||
if (subSchema.type === "number" || subSchema.type === "integer") {
|
||||
return DataType.NUMBER;
|
||||
}
|
||||
if (subSchema.type === "array") {
|
||||
/** Commented code below since we haven't yet support rendering of a multi-select with array { items: enum } type */
|
||||
// if ("items" in subSchema && subSchema.items && "enum" in subSchema.items) {
|
||||
// return DataType.MULTI_SELECT; // array + enum => multi-select
|
||||
// }
|
||||
return DataType.ARRAY;
|
||||
}
|
||||
if (subSchema.type === "object") {
|
||||
if (
|
||||
("additionalProperties" in subSchema && subSchema.additionalProperties) ||
|
||||
!("properties" in subSchema)
|
||||
) {
|
||||
return DataType.KEY_VALUE; // if additionalProperties / no properties => key-value
|
||||
}
|
||||
if (
|
||||
Object.values(subSchema.properties).every(
|
||||
(prop) => prop.type === "boolean",
|
||||
)
|
||||
) {
|
||||
return DataType.MULTI_SELECT; // if all props are boolean => multi-select
|
||||
}
|
||||
return DataType.OBJECT;
|
||||
}
|
||||
return DataType.SHORT_TEXT;
|
||||
}
|
||||
|
||||
export function determineDataType(schema: BlockIOSubSchema): DataType {
|
||||
if ("allOf" in schema) {
|
||||
// If this happens, that is because Pydantic wraps $refs in an allOf if the
|
||||
// $ref has sibling schema properties (which isn't technically allowed),
|
||||
// so there will only be one item in allOf[].
|
||||
// this should NEVER happen though, as $refs are resolved server-side.
|
||||
console.warn(
|
||||
`Detected 'allOf' wrapper: ${schema}. Normalizing use ${schema.allOf[0]} instead.`,
|
||||
);
|
||||
schema = schema.allOf[0];
|
||||
}
|
||||
|
||||
// Credentials override
|
||||
if ("credentials_provider" in schema) {
|
||||
return DataType.CREDENTIALS;
|
||||
}
|
||||
|
||||
// enum == SELECT
|
||||
if ("enum" in schema) {
|
||||
return DataType.SELECT;
|
||||
}
|
||||
|
||||
// Handle anyOf => optional types (string|null, number|null, etc.)
|
||||
if ("anyOf" in schema) {
|
||||
// e.g. schema.anyOf might look like [{ type: "string", ... }, { type: "null" }]
|
||||
const types = schema.anyOf.map((sub) =>
|
||||
"type" in sub ? sub.type : undefined,
|
||||
);
|
||||
|
||||
// (string | null)
|
||||
if (types.includes("string") && types.includes("null")) {
|
||||
const strSchema = schema.anyOf.find(
|
||||
(s) => s.type === "string",
|
||||
) as BlockIOStringSubSchema;
|
||||
return _handleStringSchema(strSchema);
|
||||
}
|
||||
|
||||
// (number|integer) & null
|
||||
if (
|
||||
(types.includes("number") || types.includes("integer")) &&
|
||||
types.includes("null")
|
||||
) {
|
||||
// Just reuse our single-type logic for whichever is not null
|
||||
const numSchema = schema.anyOf.find(
|
||||
(s) => s.type === "number" || s.type === "integer",
|
||||
);
|
||||
if (numSchema) {
|
||||
return _handleSingleTypeSchema(numSchema);
|
||||
}
|
||||
return DataType.NUMBER; // fallback
|
||||
}
|
||||
|
||||
// (array | null)
|
||||
if (types.includes("array") && types.includes("null")) {
|
||||
const arrSchema = schema.anyOf.find((s) => s.type === "array");
|
||||
if (arrSchema) return _handleSingleTypeSchema(arrSchema);
|
||||
return DataType.ARRAY;
|
||||
}
|
||||
|
||||
// (object | null)
|
||||
if (types.includes("object") && types.includes("null")) {
|
||||
const objSchema = schema.anyOf.find(
|
||||
(s) => s.type === "object",
|
||||
) as BlockIOObjectSubSchema;
|
||||
if (objSchema) return _handleSingleTypeSchema(objSchema);
|
||||
return DataType.OBJECT;
|
||||
}
|
||||
}
|
||||
|
||||
// oneOf + discriminator => user picks which variant => SELECT
|
||||
if ("oneOf" in schema && "discriminator" in schema && schema.discriminator) {
|
||||
return DataType.SELECT;
|
||||
}
|
||||
|
||||
// Direct type
|
||||
if ("type" in schema) {
|
||||
return _handleSingleTypeSchema(schema);
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return DataType.SHORT_TEXT;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user