feat: new tool mode dialog and UI (#7647)
* Add tools input to the backend * Add tools type * Add tools component to parameter render * Added tools to langflow supported types * Add tools modal * Instantiate tools component in parameter render * Removed div when there is not any visible actions * Added margin * Add ComboBoxItem and ListItem components for enhanced selection UI * Update ComboBoxItem to display item description and enhance ToolsModal layout * Refactor ToolsModal header styling for improved layout and icon padding * Enhance ComboBoxItem layout and styling; update ToolsModal size and class for better responsiveness * Changed display name * Adds truncate for badges * Adds custom styling for table used for Tools modal * Added Tools modal with AgGrid * Changed button * made name and description editable * Parse values for saving * Add focused row without triggering checkbox click * [autofix.ci] apply automated fixes * Added types for tools modal * added toolsTable with sidebar on toolsModal * Added changes to work with MCP * update component.py to add display name and display description in tool mode * removed editing directly * Fixed editing * removed to upper case * Make editing apply filters * Adds design changes for MCP * Adds new design and null check * 📝 (frontend): add data-testid attribute to elements for testing purposes 🔧 (frontend): update data-testid attribute values for consistency and clarity in testing ✅ (frontend): update tests to use correct selectors and improve test coverage for editing tools functionality * ✅ (edit-tools.spec.ts): update test to use a more reliable method for checking visibility of an element * ✅ (edit-tools.spec.ts): add "@components" tag to the test to categorize it under components for better organization and filtering in test suites. * Updated design with new design * update padding * send args to tools data * Implemented showing arguments passed to LLM in tool mode * add componentg name to description * update package lock * fixed tests * fixed backend test * fixed backend test * fixed formatting * Fixed frontend tests * updated font sizes for badges on actions and styling on sortable list * Update tool mode design * added tooltips for info * tool name update * Update component_tool.py * styling utils * default values change * fixed tools test * fix format issues --------- Co-authored-by: deon-sanchez <deon.sanchez@datastax.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
This commit is contained in:
parent
6019b3ee06
commit
b1217f96fe
30 changed files with 1045 additions and 188 deletions
|
|
@ -273,12 +273,14 @@ class ComposioBaseComponent(Component):
|
|||
|
||||
def configure_tools(self, toolset: ComposioToolSet) -> list[Tool]:
|
||||
tools = toolset.get_tools(actions=self._actions_data.keys())
|
||||
logger.info(f"Tools: {tools}")
|
||||
configured_tools = []
|
||||
for tool in tools:
|
||||
# Set the sanitized name
|
||||
tool.name = self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
|
||||
display_name = self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
|
||||
# Set the tags
|
||||
tool.tags = [tool.name]
|
||||
tool.metadata = {"display_name": display_name, "display_description": tool.description}
|
||||
configured_tools.append(tool)
|
||||
return configured_tools
|
||||
|
||||
|
|
|
|||
|
|
@ -42,19 +42,19 @@ def _get_input_type(input_: InputTypes):
|
|||
def build_description(component: Component, output: Output) -> str:
|
||||
if not output.required_inputs:
|
||||
logger.warning(f"Output {output.name} does not have required inputs defined")
|
||||
|
||||
name = component.name or component.__class__.__name__
|
||||
if output.required_inputs:
|
||||
args = ", ".join(
|
||||
sorted(
|
||||
[
|
||||
f"{input_name}: {_get_input_type(component._inputs[input_name])}"
|
||||
f"{name}. {input_name}: {_get_input_type(component._inputs[input_name])}"
|
||||
for input_name in output.required_inputs
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
args = ""
|
||||
return f"{output.method}({args}) - {component.description}"
|
||||
return f"{name}. {output.method}({args}) - {component.description}"
|
||||
|
||||
|
||||
async def send_message_noop(
|
||||
|
|
@ -239,7 +239,7 @@ class ComponentToolkit:
|
|||
else:
|
||||
args_schema = create_input_schema(self.component.inputs)
|
||||
|
||||
name = f"{self.component.name or self.component.__class__.__name__ or ''}.{output.method}".strip(".")
|
||||
name = f"{output.method}".strip(".")
|
||||
formatted_name = _format_tool_name(name)
|
||||
event_manager = self.component._event_manager
|
||||
if asyncio.iscoroutinefunction(output_method):
|
||||
|
|
@ -252,6 +252,10 @@ class ComponentToolkit:
|
|||
handle_tool_error=True,
|
||||
callbacks=callbacks,
|
||||
tags=[formatted_name],
|
||||
metadata={
|
||||
"display_name": formatted_name,
|
||||
"display_description": build_description(self.component, output),
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
|
@ -264,6 +268,10 @@ class ComponentToolkit:
|
|||
handle_tool_error=True,
|
||||
callbacks=callbacks,
|
||||
tags=[formatted_name],
|
||||
metadata={
|
||||
"display_name": formatted_name,
|
||||
"display_description": build_description(self.component, output),
|
||||
},
|
||||
)
|
||||
)
|
||||
if len(tools) == 1 and (tool_name or tool_description):
|
||||
|
|
|
|||
|
|
@ -394,6 +394,6 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
|
|||
|
||||
def set_default_tools(self):
|
||||
self._default_tools = {
|
||||
self.sanitize_action_name("GMAIL_SEND_EMAIL").replace(" ", "-"),
|
||||
self.sanitize_action_name("GMAIL_FETCH_EMAILS").replace(" ", "-"),
|
||||
"GMAIL_SEND_EMAIL",
|
||||
"GMAIL_FETCH_EMAILS",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -581,6 +581,6 @@ class ComposioSlackAPIComponent(ComposioBaseComponent):
|
|||
|
||||
def set_default_tools(self):
|
||||
self._default_tools = {
|
||||
self.sanitize_action_name("SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL").replace(" ", "-"),
|
||||
self.sanitize_action_name("SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY").replace(" ", "-"),
|
||||
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
|
||||
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ from pydantic import BaseModel, ValidationError
|
|||
from langflow.base.tools.constants import (
|
||||
TOOL_OUTPUT_DISPLAY_NAME,
|
||||
TOOL_OUTPUT_NAME,
|
||||
TOOL_TABLE_SCHEMA,
|
||||
TOOLS_METADATA_INFO,
|
||||
TOOLS_METADATA_INPUT_NAME,
|
||||
)
|
||||
|
|
@ -33,7 +32,6 @@ from langflow.schema.artifact import get_artifact_type, post_process_raw
|
|||
from langflow.schema.data import Data
|
||||
from langflow.schema.message import ErrorMessage, Message
|
||||
from langflow.schema.properties import Source
|
||||
from langflow.schema.table import FieldParserType, TableOptions
|
||||
from langflow.services.tracing.schema import Log
|
||||
from langflow.template.field.base import UNDEFINED, Input, Output
|
||||
from langflow.template.frontend_node.custom_components import ComponentFrontendNode
|
||||
|
|
@ -1174,10 +1172,14 @@ class Component(CustomComponent):
|
|||
"description": tool.description,
|
||||
"tags": tool.tags if hasattr(tool, "tags") and tool.tags else [tool.name],
|
||||
"status": True, # Initialize all tools with status True
|
||||
"display_name": tool.metadata.get("display_name", tool.name),
|
||||
"display_description": tool.metadata.get("display_description", tool.description),
|
||||
"args": tool.args,
|
||||
# "args_schema": tool.args_schema,
|
||||
}
|
||||
for tool in tools
|
||||
]
|
||||
|
||||
# print(tool_data)
|
||||
if hasattr(self, TOOLS_METADATA_INPUT_NAME):
|
||||
old_tags = self._extract_tools_tags(self.tools_metadata)
|
||||
new_tags = self._extract_tools_tags(tool_data)
|
||||
|
|
@ -1203,35 +1205,16 @@ class Component(CustomComponent):
|
|||
self.tools_metadata = tool_data
|
||||
|
||||
try:
|
||||
from langflow.io import TableInput
|
||||
from langflow.io import ToolsInput
|
||||
except ImportError as e:
|
||||
msg = "Failed to import TableInput from langflow.io"
|
||||
msg = "Failed to import ToolsInput from langflow.io"
|
||||
raise ImportError(msg) from e
|
||||
|
||||
return TableInput(
|
||||
return ToolsInput(
|
||||
name=TOOLS_METADATA_INPUT_NAME,
|
||||
display_name="Edit tools",
|
||||
real_time_refresh=True,
|
||||
table_schema=TOOL_TABLE_SCHEMA,
|
||||
display_name="Actions",
|
||||
info=TOOLS_METADATA_INFO,
|
||||
value=tool_data,
|
||||
table_icon="Hammer",
|
||||
trigger_icon="Hammer",
|
||||
trigger_text="",
|
||||
table_options=TableOptions(
|
||||
block_add=True,
|
||||
block_delete=True,
|
||||
block_edit=True, # Allow editing for status toggle
|
||||
block_sort=True,
|
||||
block_filter=True,
|
||||
block_hide=True,
|
||||
block_select=True,
|
||||
hide_options=True,
|
||||
field_parsers={
|
||||
"name": [FieldParserType.SNAKE_CASE, FieldParserType.NO_BLANK],
|
||||
"commands": FieldParserType.COMMANDS,
|
||||
},
|
||||
description=TOOLS_METADATA_INFO,
|
||||
),
|
||||
)
|
||||
|
||||
def get_project_name(self):
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ class ParameterHandler:
|
|||
params[field_name] = val
|
||||
case str():
|
||||
params[field_name] = bool(val)
|
||||
case "table":
|
||||
case "table" | "tools":
|
||||
if isinstance(val, list) and all(isinstance(item, dict) for item in val):
|
||||
params[field_name] = pd.DataFrame(val)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from .inputs import (
|
|||
StrInput,
|
||||
TabInput,
|
||||
TableInput,
|
||||
ToolsInput,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -62,4 +63,5 @@ __all__ = [
|
|||
"StrInput",
|
||||
"TabInput",
|
||||
"TableInput",
|
||||
"ToolsInput",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class FieldTypes(str, Enum):
|
|||
SLIDER = "slider"
|
||||
TAB = "tab"
|
||||
QUERY = "query"
|
||||
TOOLS = "tools"
|
||||
|
||||
|
||||
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
||||
|
|
|
|||
|
|
@ -87,6 +87,21 @@ class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin):
|
|||
field_type: SerializableFieldTypes = FieldTypes.OTHER
|
||||
|
||||
|
||||
class ToolsInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolModeMixin):
|
||||
"""Represents an Input that contains a list of tools to activate, deactivate, or edit.
|
||||
|
||||
Attributes:
|
||||
field_type (SerializableFieldTypes): The field type of the input.
|
||||
value (list[dict]): The value of the input.
|
||||
|
||||
"""
|
||||
|
||||
field_type: SerializableFieldTypes = FieldTypes.TOOLS
|
||||
value: list[dict] = Field(default_factory=list)
|
||||
is_list: bool = True
|
||||
real_time_refresh: bool = True
|
||||
|
||||
|
||||
class DataInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin):
|
||||
"""Represents an Input that has a Handle that receives a Data object.
|
||||
|
||||
|
|
@ -644,6 +659,7 @@ InputTypes: TypeAlias = (
|
|||
| MultilineInput
|
||||
| MultilineSecretInput
|
||||
| NestedDictInput
|
||||
| ToolsInput
|
||||
| PromptInput
|
||||
| CodeInput
|
||||
| SecretStrInput
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from langflow.inputs import (
|
|||
StrInput,
|
||||
TabInput,
|
||||
TableInput,
|
||||
ToolsInput,
|
||||
)
|
||||
from langflow.template import Output
|
||||
|
||||
|
|
@ -57,4 +58,5 @@ __all__ = [
|
|||
"StrInput",
|
||||
"TabInput",
|
||||
"TableInput",
|
||||
"ToolsInput",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ DIRECT_TYPES = [
|
|||
"auth",
|
||||
"connect",
|
||||
"query",
|
||||
"tools",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def test_component_tool():
|
|||
calculator_component = CalculatorToolComponent()
|
||||
component_toolkit = ComponentToolkit(component=calculator_component)
|
||||
component_tool = component_toolkit.get_tools()[0]
|
||||
assert component_tool.name == "CalculatorTool-run_model"
|
||||
assert component_tool.name == "run_model"
|
||||
assert issubclass(component_tool.args_schema, BaseModel)
|
||||
# TODO: fix this
|
||||
# assert component_tool.args_schema.model_json_schema()["properties"] == {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ def test_component_inputs_toolkit():
|
|||
component = AllInputsComponent()
|
||||
component_toolkit = ComponentToolkit(component=component)
|
||||
component_tool = component_toolkit.get_tools()[0]
|
||||
assert component_tool.name == "AllInputsComponent-build_output"
|
||||
assert component_tool.name == "build_output"
|
||||
assert issubclass(component_tool.args_schema, BaseModel)
|
||||
properties = component_tool.args_schema.model_json_schema()["properties"]
|
||||
|
||||
|
|
|
|||
4
src/frontend/package-lock.json
generated
4
src/frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "langflow",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langflow",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"dependencies": {
|
||||
"@chakra-ui/number-input": "^2.1.2",
|
||||
"@headlessui/react": "^2.0.4",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ComboBoxItemProps {
|
||||
item: any;
|
||||
}
|
||||
|
||||
const ComboBoxItem = ({ item }: ComboBoxItemProps) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row pb-1">
|
||||
<div className="inline-flex justify-start">
|
||||
<label
|
||||
className="relative flex cursor-pointer"
|
||||
htmlFor={`check-${item?.name}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => setIsChecked(!isChecked)}
|
||||
className="peer h-5 w-5 cursor-pointer appearance-none rounded border border-border border-muted-foreground shadow transition-all hover:shadow-md"
|
||||
id={`check-${item?.name}`}
|
||||
/>
|
||||
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-black opacity-0 peer-checked:bg-primary peer-checked:opacity-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-3.5 w-3.5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
unstyled
|
||||
className="mx-2 flex w-full justify-start rounded-md hover:bg-accent"
|
||||
>
|
||||
<label
|
||||
className="ml-2 flex w-72 cursor-pointer text-sm font-bold text-primary"
|
||||
htmlFor={`check-${item?.name}`}
|
||||
>
|
||||
<span className="truncate">{item?.name}</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
className="flex w-72 cursor-pointer text-sm text-muted-foreground"
|
||||
htmlFor={`check-${item?.name}`}
|
||||
>
|
||||
<span className="truncate">{item?.description}</span>
|
||||
</label>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ComboBoxItem;
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const ListItem = ({
|
||||
item,
|
||||
isSelected,
|
||||
onClick,
|
||||
className,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
isFocused,
|
||||
isKeyboardNavActive,
|
||||
dataTestId,
|
||||
}: {
|
||||
item: any;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
isFocused: boolean;
|
||||
isKeyboardNavActive: boolean;
|
||||
dataTestId: string;
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const itemRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Clear hover state when keyboard navigation is active
|
||||
useEffect(() => {
|
||||
if (isKeyboardNavActive) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}, [isKeyboardNavActive]);
|
||||
|
||||
// Scroll into view when focused by keyboard
|
||||
useEffect(() => {
|
||||
if (isFocused && itemRef.current) {
|
||||
itemRef.current.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={itemRef}
|
||||
key={item.id}
|
||||
data-testid={dataTestId}
|
||||
unstyled
|
||||
size="sm"
|
||||
className={cn(
|
||||
"group flex w-full rounded-md px-2 py-0.5",
|
||||
!isKeyboardNavActive && "hover:bg-muted", // Only apply hover styles when not in keyboard nav
|
||||
!item.metaData && "py-2.5",
|
||||
isFocused && "bg-muted",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => {
|
||||
if (!isKeyboardNavActive) {
|
||||
setIsHovered(true);
|
||||
onMouseEnter();
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
onMouseLeave();
|
||||
}}
|
||||
// Disable pointer events during keyboard navigation
|
||||
style={{ pointerEvents: isKeyboardNavActive ? "none" : "auto" }}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{item.icon && (
|
||||
<div>
|
||||
<ForwardedIconComponent name={item.icon} className="mr-2 h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col truncate">
|
||||
<div className="flex w-full truncate text-mmd font-semibold">
|
||||
<span className="truncate">{item.name}</span>
|
||||
</div>
|
||||
{"metaData" in item && item.metaData && (
|
||||
<div className="flex w-full truncate text-mmd text-gray-500">
|
||||
<span className="truncate">{item.metaData}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isHovered || isFocused ? (
|
||||
<div className="ml-auto flex items-center justify-start rounded-md">
|
||||
<div className="flex items-center pr-1.5 text-mmd font-semibold text-muted-foreground">
|
||||
Select
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded-md">
|
||||
<ForwardedIconComponent
|
||||
name="corner-down-left"
|
||||
className="h-3.5 w-3.5 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Always show the check icon when selected, regardless of hover/focus state
|
||||
isSelected && (
|
||||
<ForwardedIconComponent
|
||||
name="check"
|
||||
className={cn(
|
||||
"ml-auto flex h-4 w-4",
|
||||
item.link === "validated" && "text-green-500",
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
|
||||
import { InputProps } from "@/components/core/parameterRenderComponent/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogHeader } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close";
|
||||
import { cn, testIdCase } from "@/utils/utils";
|
||||
import { testIdCase } from "@/utils/utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ListItem from "./ListItem";
|
||||
|
||||
// Update interface with better types
|
||||
interface ListSelectionComponentProps {
|
||||
|
|
@ -20,118 +19,6 @@ interface ListSelectionComponentProps {
|
|||
limit?: number;
|
||||
}
|
||||
|
||||
const ListItem = ({
|
||||
item,
|
||||
isSelected,
|
||||
onClick,
|
||||
className,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
isFocused,
|
||||
isKeyboardNavActive,
|
||||
dataTestId,
|
||||
}: {
|
||||
item: any;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
isFocused: boolean;
|
||||
isKeyboardNavActive: boolean;
|
||||
dataTestId: string;
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const itemRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Clear hover state when keyboard navigation is active
|
||||
useEffect(() => {
|
||||
if (isKeyboardNavActive) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}, [isKeyboardNavActive]);
|
||||
|
||||
// Scroll into view when focused by keyboard
|
||||
useEffect(() => {
|
||||
if (isFocused && itemRef.current) {
|
||||
itemRef.current.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={itemRef}
|
||||
key={item.id}
|
||||
data-testid={dataTestId}
|
||||
unstyled
|
||||
size="sm"
|
||||
className={cn(
|
||||
"group flex w-full rounded-md px-2 py-0.5",
|
||||
!isKeyboardNavActive && "hover:bg-muted", // Only apply hover styles when not in keyboard nav
|
||||
!item.metaData && "py-2.5",
|
||||
isFocused && "bg-muted",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => {
|
||||
if (!isKeyboardNavActive) {
|
||||
setIsHovered(true);
|
||||
onMouseEnter();
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
onMouseLeave();
|
||||
}}
|
||||
// Disable pointer events during keyboard navigation
|
||||
style={{ pointerEvents: isKeyboardNavActive ? "none" : "auto" }}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{item.icon && (
|
||||
<div>
|
||||
<ForwardedIconComponent name={item.icon} className="mr-2 h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col truncate">
|
||||
<div className="flex w-full truncate text-mmd font-semibold">
|
||||
<span className="truncate">{item.name}</span>
|
||||
</div>
|
||||
{"metaData" in item && item.metaData && (
|
||||
<div className="flex w-full truncate text-mmd text-gray-500">
|
||||
<span className="truncate">{item.metaData}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isHovered || isFocused ? (
|
||||
<div className="ml-auto flex items-center justify-start rounded-md">
|
||||
<div className="flex items-center pr-1.5 text-mmd font-semibold text-muted-foreground">
|
||||
Select
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded-md">
|
||||
<ForwardedIconComponent
|
||||
name="corner-down-left"
|
||||
className="h-3.5 w-3.5 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Always show the check icon when selected, regardless of hover/focus state
|
||||
isSelected && (
|
||||
<ForwardedIconComponent
|
||||
name="check"
|
||||
className={cn(
|
||||
"ml-auto flex h-4 w-4",
|
||||
item.link === "validated" && "text-green-500",
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const ListSelectionComponent = ({
|
||||
open,
|
||||
onClose,
|
||||
|
|
@ -286,7 +173,7 @@ const ListSelectionComponent = ({
|
|||
name={nodeClass?.icon || "unknown"}
|
||||
className="h-[18px] w-[18px] text-muted-foreground"
|
||||
/>
|
||||
<div className="text-mmd font-semibold">
|
||||
<div className="text-[13px] font-semibold">
|
||||
{nodeClass?.display_name}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ const RenderInputParameters = ({
|
|||
(templateField: string, idx) => {
|
||||
const template = data.node?.template[templateField];
|
||||
|
||||
if (!template?.show || template?.advanced) {
|
||||
if (
|
||||
!template?.show ||
|
||||
template?.advanced ||
|
||||
(template?.tool_mode && isToolMode)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||
import ToolsModal from "@/modals/toolsModal";
|
||||
import { cn, testIdCase } from "@/utils/utils";
|
||||
import { useState } from "react";
|
||||
import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
|
||||
import { Badge } from "../../../../ui/badge";
|
||||
import { Button } from "../../../../ui/button";
|
||||
import { Skeleton } from "../../../../ui/skeleton";
|
||||
import { InputProps, ToolsComponentType } from "../../types";
|
||||
|
||||
export default function ToolsComponent({
|
||||
description,
|
||||
value,
|
||||
editNode = false,
|
||||
id = "",
|
||||
handleOnNewValue,
|
||||
isAction = false,
|
||||
button_description,
|
||||
title,
|
||||
icon,
|
||||
disabled = false,
|
||||
template,
|
||||
}: InputProps<any[] | undefined, ToolsComponentType>): JSX.Element {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const actions = value
|
||||
?.filter((action) => action.status === true)
|
||||
.map((action) => {
|
||||
return {
|
||||
name: action.name,
|
||||
tags: action.tags,
|
||||
description: action.description,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleActionsQt = isAction ? 20 : 4;
|
||||
|
||||
const visibleActions = actions?.slice(0, visibleActionsQt) || [];
|
||||
const remainingCount = actions
|
||||
? Math.max(0, actions.length - visibleActionsQt)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center",
|
||||
disabled && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{value && (
|
||||
<ToolsModal
|
||||
open={isModalOpen}
|
||||
setOpen={setIsModalOpen}
|
||||
isAction={isAction}
|
||||
description={description}
|
||||
template={template}
|
||||
rows={value}
|
||||
handleOnNewValue={handleOnNewValue}
|
||||
title={title}
|
||||
icon={icon}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="relative flex w-full items-center gap-3"
|
||||
data-testid={"div-" + id}
|
||||
>
|
||||
{(visibleActions.length > 0 || isAction) && (
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
disabled={!value || disabled}
|
||||
size={"iconMd"}
|
||||
className={cn(
|
||||
"absolute -top-8 right-0 font-semibold text-muted-foreground group-hover:text-primary",
|
||||
)}
|
||||
data-testid="button_open_actions"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Settings2"
|
||||
className="icon-size"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
/>
|
||||
{button_description}
|
||||
</Button>
|
||||
)}
|
||||
{!value ? (
|
||||
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
|
||||
{[...Array(4)].map((_, index) => (
|
||||
<Skeleton key={index} className="h-6 w-20 rounded-full" />
|
||||
))}
|
||||
</div>
|
||||
) : visibleActions.length > 0 ? (
|
||||
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
|
||||
{visibleActions.map((action, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondaryStatic"
|
||||
size="sq"
|
||||
className="truncate font-normal"
|
||||
data-testid={testIdCase(`tool_${action.name}`)}
|
||||
>
|
||||
<span className="truncate text-xxs font-medium">
|
||||
{action.name.toUpperCase()}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<span className="ml-1 self-center text-xs font-normal text-muted-foreground">
|
||||
+{remainingCount} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
visibleActions.length === 0 &&
|
||||
isAction && (
|
||||
<div className="mt-2 flex w-full flex-col items-center gap-2 rounded-md border border-dashed p-8">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No actions added to this server
|
||||
</span>
|
||||
<Button size={"sm"} onClick={() => setIsModalOpen(true)}>
|
||||
<span>Add actions</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{visibleActions.length === 0 && !isAction && value && (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size={editNode ? "xs" : "default"}
|
||||
className={
|
||||
"w-full " +
|
||||
(disabled ? "pointer-events-none cursor-not-allowed" : "")
|
||||
}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<span>Select actions</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,27 +32,27 @@ const SortableListItem = memo(
|
|||
}) => (
|
||||
<li
|
||||
className={cn(
|
||||
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium text-gray-800",
|
||||
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium",
|
||||
limit === 1 ? "h-6 rounded-md bg-muted" : "group cursor-grab",
|
||||
)}
|
||||
>
|
||||
{limit !== 1 && (
|
||||
<ForwardedIconComponent
|
||||
name="grid-horizontal"
|
||||
className="h-5 w-5 fill-gray-300 text-gray-300"
|
||||
name="GridHorizontal"
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
{limit !== 1 && (
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-400 text-center text-white">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-border text-center text-mmd text-primary">
|
||||
{index + 1}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-muted-foreground",
|
||||
"truncate text-xxs font-medium text-muted-foreground",
|
||||
limit === 1 ? "max-w-56 pl-2" : "max-w-48",
|
||||
)}
|
||||
>
|
||||
|
|
@ -61,22 +61,16 @@ const SortableListItem = memo(
|
|||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant={limit !== 1 ? "outline" : "ghost"}
|
||||
variant={"ghost"}
|
||||
className={cn(
|
||||
"ml-auto h-6 w-6 opacity-0 transition-opacity duration-200",
|
||||
"ml-auto h-6 w-6 text-muted-foreground opacity-0 transition-opacity duration-200",
|
||||
limit === 1
|
||||
? "group pr-1 opacity-100"
|
||||
: "hover:border hover:border-destructive hover:bg-transparent hover:opacity-100",
|
||||
? "group pr-1 opacity-100 hover:text-foreground"
|
||||
: "hover:text-destructive group-hover:opacity-100",
|
||||
)}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="x"
|
||||
className={cn(
|
||||
"h-6 w-6 text-red-500",
|
||||
limit === 1 && "text-gray-500 group-hover:text-input",
|
||||
)}
|
||||
/>
|
||||
<ForwardedIconComponent name="x" className={cn("h-6 w-6")} />
|
||||
</Button>
|
||||
</li>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/
|
|||
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { useMemo } from "react";
|
||||
import ToolsComponent from "./components/ToolsComponent";
|
||||
import ConnectionComponent from "./components/connectionComponent";
|
||||
import DictComponent from "./components/dictComponent";
|
||||
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||
|
|
@ -203,6 +204,16 @@ export function ParameterRenderComponent({
|
|||
table_icon={templateData?.table_icon}
|
||||
/>
|
||||
);
|
||||
case "tools":
|
||||
return (
|
||||
<ToolsComponent
|
||||
{...baseInputProps}
|
||||
description={templateData.info || "Add or edit data"}
|
||||
title={nodeClass?.display_name ?? "Tools"}
|
||||
icon={nodeClass?.icon ?? ""}
|
||||
template={nodeClass?.template}
|
||||
/>
|
||||
);
|
||||
case "slider":
|
||||
return (
|
||||
<SliderComponent
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { APIClassType, InputFieldType, TableOptionsTypeAPI } from "@/types/api";
|
||||
import {
|
||||
APIClassType,
|
||||
APITemplateType,
|
||||
InputFieldType,
|
||||
TableOptionsTypeAPI,
|
||||
} from "@/types/api";
|
||||
import { RangeSpecType } from "@/types/components";
|
||||
import { ColumnField } from "@/types/utils/functions";
|
||||
|
||||
|
|
@ -40,6 +45,15 @@ export type TableComponentType = {
|
|||
table_icon?: string;
|
||||
};
|
||||
|
||||
export type ToolsComponentType = {
|
||||
description: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
button_description?: string;
|
||||
isAction?: boolean;
|
||||
template?: APITemplateType;
|
||||
};
|
||||
|
||||
export type FloatComponentType = {
|
||||
rangeSpec: RangeSpecType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -659,6 +659,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
"connect",
|
||||
"auth",
|
||||
"query",
|
||||
"tools",
|
||||
]);
|
||||
|
||||
export const FLEX_VIEW_TYPES = ["bool"];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,386 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import TableComponent from "@/components/core/parameterRenderComponent/components/tableComponent";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { APITemplateType } from "@/types/api";
|
||||
import { parseString } from "@/utils/stringManipulation";
|
||||
import { ColDef } from "ag-grid-community";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
export default function ToolsTable({
|
||||
rows,
|
||||
data,
|
||||
setData,
|
||||
isAction,
|
||||
open,
|
||||
handleOnNewValue,
|
||||
template,
|
||||
}: {
|
||||
rows: any[];
|
||||
data: any[];
|
||||
template?: APITemplateType;
|
||||
setData: (data: any[]) => void;
|
||||
open: boolean;
|
||||
handleOnNewValue: handleOnNewValueType;
|
||||
isAction: boolean;
|
||||
}) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedRows, setSelectedRows] = useState<any[] | null>(null);
|
||||
const agGrid = useRef<AgGridReact>(null);
|
||||
|
||||
const [focusedRow, setFocusedRow] = useState<any | null>(null);
|
||||
const [sidebarName, setSidebarName] = useState<string>("");
|
||||
const [sidebarDescription, setSidebarDescription] = useState<string>("");
|
||||
|
||||
const { setOpen: setSidebarOpen } = useSidebar();
|
||||
|
||||
const getRowId = useMemo(() => {
|
||||
return (params: any) => params.data.display_name ?? params.data.name;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const initialData = cloneDeep(rows);
|
||||
setData(initialData);
|
||||
const filter = initialData.filter((row) => row.status === true);
|
||||
setSelectedRows(filter);
|
||||
}, [rows, open]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialData = cloneDeep(rows);
|
||||
const filter = initialData.filter((row) => row.status === true);
|
||||
if (agGrid.current) {
|
||||
agGrid.current?.api?.forEachNode((node) => {
|
||||
if (
|
||||
filter.some(
|
||||
(row) =>
|
||||
(row.display_name ?? row.name) ===
|
||||
(node.data.display_name ?? node.data.name),
|
||||
)
|
||||
) {
|
||||
node.setSelected(true);
|
||||
} else {
|
||||
node.setSelected(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [agGrid.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open && selectedRows) {
|
||||
handleOnNewValue({
|
||||
value: data.map((row) => {
|
||||
const processedValue = (
|
||||
row.name !== ""
|
||||
? parseString(row.name, ["snake_case", "no_blank", "lowercase"])
|
||||
: parseString(row.display_name, [
|
||||
"snake_case",
|
||||
"no_blank",
|
||||
"lowercase",
|
||||
])
|
||||
).slice(0, 46);
|
||||
|
||||
const processedDescription =
|
||||
row.description !== "" ? row.description : row.display_description;
|
||||
|
||||
return selectedRows?.some(
|
||||
(selected) =>
|
||||
(selected.display_name ?? selected.name) ===
|
||||
(row.display_name ?? row.name),
|
||||
)
|
||||
? {
|
||||
...row,
|
||||
status: true,
|
||||
name: processedValue,
|
||||
description: processedDescription,
|
||||
}
|
||||
: {
|
||||
...row,
|
||||
status: false,
|
||||
name: processedValue,
|
||||
description: processedDescription,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedRow) {
|
||||
setSidebarName(focusedRow.name);
|
||||
setSidebarDescription(focusedRow.description);
|
||||
} else {
|
||||
setSidebarName("");
|
||||
setSidebarDescription("");
|
||||
}
|
||||
}, [focusedRow]);
|
||||
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
field: isAction ? "display_name" : "name",
|
||||
headerName: isAction ? "Flow Name" : "Name",
|
||||
flex: 1,
|
||||
valueGetter: (params) =>
|
||||
!isAction
|
||||
? parseString(
|
||||
params.data.display_name !== ""
|
||||
? params.data.display_name
|
||||
: params.data.name,
|
||||
["space_case"],
|
||||
)
|
||||
: params.data.display_name,
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
headerName: "Description",
|
||||
flex: 2,
|
||||
cellClass: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
field: isAction ? "name" : "tags",
|
||||
headerName: isAction ? "Action" : "Slug",
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
valueGetter: (params) =>
|
||||
isAction
|
||||
? params.data.name !== ""
|
||||
? parseString(params.data.name, [
|
||||
"snake_case",
|
||||
"no_blank",
|
||||
"uppercase",
|
||||
])
|
||||
: parseString(params.data.display_name, [
|
||||
"snake_case",
|
||||
"no_blank",
|
||||
"uppercase",
|
||||
])
|
||||
: parseString(params.data.tags.join(", "), [
|
||||
"snake_case",
|
||||
"uppercase",
|
||||
]),
|
||||
cellClass: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
field: "tags",
|
||||
headerName: "Tags",
|
||||
flex: 1,
|
||||
hide: true,
|
||||
},
|
||||
];
|
||||
const handleSelectionChanged = (event) => {
|
||||
if (open) {
|
||||
const selectedData = event.api.getSelectedRows();
|
||||
setSelectedRows(selectedData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSidebarInputChange = (
|
||||
field: "name" | "description",
|
||||
value: string,
|
||||
) => {
|
||||
if (!focusedRow) return;
|
||||
|
||||
const originalName = focusedRow.display_name;
|
||||
|
||||
setFocusedRow((prev) => (prev ? { ...prev, [field]: value } : null));
|
||||
|
||||
if (agGrid.current) {
|
||||
const updatedRow = { ...focusedRow, [field]: value };
|
||||
|
||||
agGrid.current.api.applyTransaction({
|
||||
update: [updatedRow],
|
||||
});
|
||||
|
||||
const updatedData = data.map((row) =>
|
||||
(row.display_name ?? row.name) === originalName ? updatedRow : row,
|
||||
);
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
const actionArgs = useMemo(() => {
|
||||
return Object.entries(focusedRow?.args ?? {}).map(
|
||||
([key, value]: [string, any]) => ({
|
||||
display_name: value.title,
|
||||
name: key,
|
||||
description: value.description ?? null,
|
||||
}),
|
||||
);
|
||||
}, [focusedRow]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex h-full w-full flex-1 flex-col gap-2 overflow-hidden py-4">
|
||||
<div className="flex-none px-4">
|
||||
<Input
|
||||
icon="Search"
|
||||
placeholder="Search actions..."
|
||||
inputClassName="h-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<TableComponent
|
||||
columnDefs={columnDefs}
|
||||
rowData={data}
|
||||
quickFilterText={searchQuery}
|
||||
ref={agGrid}
|
||||
rowSelection="multiple"
|
||||
suppressRowClickSelection={true}
|
||||
className="ag-tool-mode h-full w-full overflow-visible"
|
||||
headerHeight={32}
|
||||
rowHeight={32}
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
tableOptions={{
|
||||
block_hide: true,
|
||||
}}
|
||||
onRowClicked={(event) => {
|
||||
setFocusedRow(event.data);
|
||||
setSidebarOpen(true);
|
||||
}}
|
||||
getRowId={getRowId}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<Sidebar
|
||||
side="right"
|
||||
className="flex h-full flex-col overflow-auto border-l border-border"
|
||||
>
|
||||
<SidebarHeader className="flex-none px-4 py-4">
|
||||
{focusedRow &&
|
||||
(isAction ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
className="text-mmd font-medium"
|
||||
htmlFor="sidebar-name-input"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
|
||||
<Input
|
||||
id="sidebar-name-input"
|
||||
value={sidebarName}
|
||||
onChange={(e) => {
|
||||
setSidebarName(e.target.value);
|
||||
handleSidebarInputChange("name", e.target.value);
|
||||
}}
|
||||
maxLength={46}
|
||||
placeholder="Edit name..."
|
||||
data-testid="input_update_name"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{isAction
|
||||
? "Used as the function name when this flow is exposed to clients."
|
||||
: "Used as the function name when this tool is exposed to the agent."}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
className="text-mmd font-medium"
|
||||
htmlFor="sidebar-desc-input"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
id="sidebar-desc-input"
|
||||
value={sidebarDescription}
|
||||
onChange={(e) => {
|
||||
setSidebarDescription(e.target.value);
|
||||
handleSidebarInputChange("description", e.target.value);
|
||||
}}
|
||||
placeholder="Edit description..."
|
||||
className="h-24"
|
||||
data-testid="input_update_description"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{isAction
|
||||
? "This is the description for the action exposed to the clients."
|
||||
: "This is the description for the tool exposed to the agents."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1" data-testid="sidebar_header">
|
||||
<h3
|
||||
className="text-base font-medium"
|
||||
data-testid="sidebar_header_name"
|
||||
>
|
||||
{parseString(focusedRow?.display_name || focusedRow?.name, [
|
||||
"space_case",
|
||||
])}
|
||||
</h3>
|
||||
<p
|
||||
className="text-mmd text-muted-foreground"
|
||||
data-testid="sidebar_header_description"
|
||||
>
|
||||
{focusedRow?.display_description ?? focusedRow?.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</SidebarHeader>
|
||||
{!isAction && <Separator />}
|
||||
<SidebarContent className="flex flex-1 flex-col gap-0 overflow-visible px-2">
|
||||
{focusedRow && (
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<SidebarGroup className="flex-1">
|
||||
<SidebarGroupContent className="h-full pb-4">
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
{actionArgs.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h3 className="mt-2 text-base font-medium">
|
||||
Parameters
|
||||
</h3>
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Manage inputs for this action
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{actionArgs.map((field) => (
|
||||
<div key={field.name} className="flex flex-col gap-2">
|
||||
<label className="flex text-sm font-medium">
|
||||
{field.display_name}
|
||||
{field.description && (
|
||||
<ShadTooltip content={field.description}>
|
||||
<div className="flex items-center text-sm font-medium hover:cursor-help">
|
||||
<ForwardedIconComponent
|
||||
name="info"
|
||||
className="ml-1.5 h-4 w-4 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</label>
|
||||
<Input
|
||||
id="sidebar-desc-input"
|
||||
disabled
|
||||
placeholder="Input controlled by the agent"
|
||||
onChange={(e) => {}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</div>
|
||||
)}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
89
src/frontend/src/modals/toolsModal/index.tsx
Normal file
89
src/frontend/src/modals/toolsModal/index.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { APITemplateType } from "@/types/api";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { ForwardedRef, forwardRef, useState } from "react";
|
||||
import BaseModal from "../baseModal";
|
||||
import ToolsTable from "./components/toolsTable";
|
||||
|
||||
interface ToolsModalProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
description: string;
|
||||
rows: {
|
||||
name: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
status: boolean;
|
||||
}[];
|
||||
handleOnNewValue: handleOnNewValueType;
|
||||
title: string;
|
||||
icon?: string;
|
||||
isAction?: boolean;
|
||||
template?: APITemplateType;
|
||||
}
|
||||
|
||||
const ToolsModal = forwardRef<AgGridReact, ToolsModalProps>(
|
||||
(
|
||||
{
|
||||
description,
|
||||
rows,
|
||||
handleOnNewValue,
|
||||
template,
|
||||
title,
|
||||
icon,
|
||||
open,
|
||||
isAction = false,
|
||||
setOpen,
|
||||
...props
|
||||
}: ToolsModalProps,
|
||||
ref: ForwardedRef<AgGridReact>,
|
||||
) => {
|
||||
const handleSetOpen = (newOpen: boolean) => {
|
||||
if (setOpen) {
|
||||
setOpen(newOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const [data, setData] = useState<any[]>(cloneDeep(rows));
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
open={open}
|
||||
size="templates"
|
||||
className="flex max-h-[50vh] gap-0 p-0"
|
||||
setOpen={(newOpen) => {
|
||||
handleSetOpen(newOpen);
|
||||
}}
|
||||
>
|
||||
<BaseModal.Header>
|
||||
<div className="flex w-full flex-row items-center border-b border-border px-4 py-3">
|
||||
{icon && (
|
||||
<ForwardedIconComponent name={icon} className="mr-2 h-6 w-6" />
|
||||
)}
|
||||
<div>{title}</div>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content overflowHidden className="flex flex-col p-0">
|
||||
<div className="flex h-full">
|
||||
<SidebarProvider width="20rem" defaultOpen={false}>
|
||||
<ToolsTable
|
||||
rows={rows}
|
||||
isAction={isAction}
|
||||
template={template}
|
||||
data={data}
|
||||
setData={setData}
|
||||
open={open}
|
||||
handleOnNewValue={handleOnNewValue}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ToolsModal;
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
--ag-panel-background-color: hsl(var(--accent)) !important;
|
||||
--ag-row-hover-color: hsl(var(--accent)) !important;
|
||||
--ag-header-height: 2.5rem !important;
|
||||
--ag-checkbox-checked-color: hsl(var(--primary)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-shadcn .ag-paging-panel {
|
||||
|
|
@ -33,7 +34,6 @@
|
|||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||
align-items: self-start;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||
|
|
@ -99,3 +99,46 @@
|
|||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-root-wrapper {
|
||||
border: none !important;
|
||||
}
|
||||
.ag-tool-mode .ag-row {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-header {
|
||||
margin-bottom: 0rem !important;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-header-cell {
|
||||
padding: 0rem 1rem !important;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-ltr .ag-header-select-all {
|
||||
margin-right: var(--ag-cell-widget-spacing);
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-paging-panel {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-cell-focus:not(.ag-cell-inline-editing) {
|
||||
border: 1px solid transparent !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-cell {
|
||||
padding: 0rem 1rem !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-row-focus {
|
||||
background-color: hsl(var(--accent)) !important;
|
||||
}
|
||||
|
||||
.ag-tool-mode .ag-row-selected:not(.ag-row-focus)::before {
|
||||
background-color: hsl(var(--background)) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ export type FieldParserType =
|
|||
| "uppercase"
|
||||
| "no_blank"
|
||||
| "valid_csv"
|
||||
| "space_case"
|
||||
| "commands";
|
||||
|
||||
export type TableOptionsTypeAPI = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { ReactFlowJsonObject } from "@xyflow/react";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { InputOutput } from "../../constants/enums";
|
||||
|
|
@ -676,6 +677,21 @@ export type textModalPropsType = {
|
|||
setOpen?: (open: boolean) => void;
|
||||
onCloseModal?: () => void;
|
||||
};
|
||||
|
||||
export interface ToolsModalProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
description: string;
|
||||
rows: {
|
||||
name: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
status: boolean;
|
||||
}[];
|
||||
handleOnNewValue: handleOnNewValueType;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
export type queryModalPropsType = {
|
||||
setValue: (value: string) => void;
|
||||
value: string;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import { FieldParserType } from "../types/api";
|
||||
|
||||
function toSnakeCase(str: string): string {
|
||||
return str.trim().replace(/\s+/g, "_");
|
||||
return str.trim().replace(/[-\s]+/g, "_");
|
||||
}
|
||||
|
||||
function toCamelCase(str: string): string {
|
||||
|
|
@ -35,6 +35,15 @@ function toUpperCase(str: string): string {
|
|||
return str?.toUpperCase();
|
||||
}
|
||||
|
||||
function toSpaceCase(str: string): string {
|
||||
return str
|
||||
.trim()
|
||||
.replace(/[_\s-]+/g, " ")
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function noBlank(str: string): string {
|
||||
const trim = str.trim();
|
||||
if (trim === "") {
|
||||
|
|
@ -101,6 +110,9 @@ export function parseString(
|
|||
case "no_blank":
|
||||
result = noBlank(result);
|
||||
break;
|
||||
case "space_case":
|
||||
result = toSpaceCase(result);
|
||||
break;
|
||||
case "valid_csv":
|
||||
result = validCsv(result);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -35,26 +35,46 @@ test(
|
|||
|
||||
await page.getByTestId("tool-mode-button").click();
|
||||
|
||||
await page.locator('[data-testid="icon-Hammer"]').nth(1).waitFor({
|
||||
await page.locator('[data-testid="icon-Hammer"]').nth(0).waitFor({
|
||||
timeout: 3000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
await page.getByTestId("icon-Hammer").nth(1).click();
|
||||
await page.waitForSelector("text=actions", { timeout: 30000 });
|
||||
|
||||
await page.waitForSelector("text=edit tools", { timeout: 30000 });
|
||||
await page.getByTestId("button_open_actions").click();
|
||||
|
||||
await page.waitForSelector("text=API Request", { timeout: 30000 });
|
||||
|
||||
const rowsCount = await page.getByRole("gridcell").count();
|
||||
|
||||
expect(rowsCount).toBeGreaterThan(3);
|
||||
|
||||
expect(await page.getByRole("switch").nth(0).isChecked()).toBe(true);
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(0).isChecked(),
|
||||
).toBe(true);
|
||||
|
||||
await page.getByRole("switch").nth(0).click();
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||
).toBe(true);
|
||||
|
||||
expect(await page.getByRole("switch").nth(0).isChecked()).toBe(false);
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||
).toBe(true);
|
||||
|
||||
await page.getByText("Save").last().click();
|
||||
await page.locator('input[data-ref="eInput"]').nth(0).click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'[data-testid="generic-node-title-arrangement"]',
|
||||
|
|
@ -63,14 +83,54 @@ test(
|
|||
},
|
||||
);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForSelector('[data-testid="div-tools_tools_metadata"]', {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
await page.getByTestId("icon-Hammer").nth(1).click();
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="div-tools_tools_metadata"]')
|
||||
.isVisible(),
|
||||
).toBe(true);
|
||||
|
||||
await page.waitForSelector("text=edit tools", { timeout: 30000 });
|
||||
await page.getByTestId("div-tools_tools_metadata").click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(await page.getByRole("switch").nth(0).isChecked()).toBe(false);
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||
).toBe(false);
|
||||
|
||||
await page.locator('input[data-ref="eInput"]').nth(3).click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||
).toBe(true);
|
||||
|
||||
await page.getByRole("gridcell").nth(0).click();
|
||||
|
||||
expect(
|
||||
await page.locator('[data-testid="sidebar_header_name"]').isVisible(),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="sidebar_header_description"]')
|
||||
.isVisible(),
|
||||
).toBe(true);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
expect(
|
||||
await page.locator('[data-testid="tool_make_requests"]').isVisible(),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue