diff --git a/src/backend/base/langflow/base/composio/composio_base.py b/src/backend/base/langflow/base/composio/composio_base.py index 3ca7da3ad..cb847bea1 100644 --- a/src/backend/base/langflow/base/composio/composio_base.py +++ b/src/backend/base/langflow/base/composio/composio_base.py @@ -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 diff --git a/src/backend/base/langflow/base/tools/component_tool.py b/src/backend/base/langflow/base/tools/component_tool.py index b8dab8f96..29be7a309 100644 --- a/src/backend/base/langflow/base/tools/component_tool.py +++ b/src/backend/base/langflow/base/tools/component_tool.py @@ -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): diff --git a/src/backend/base/langflow/components/composio/gmail_composio.py b/src/backend/base/langflow/components/composio/gmail_composio.py index a6f0c9950..01bb8296d 100644 --- a/src/backend/base/langflow/components/composio/gmail_composio.py +++ b/src/backend/base/langflow/components/composio/gmail_composio.py @@ -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", } diff --git a/src/backend/base/langflow/components/composio/slack_composio.py b/src/backend/base/langflow/components/composio/slack_composio.py index fc97b2bd1..80d45ebf3 100644 --- a/src/backend/base/langflow/components/composio/slack_composio.py +++ b/src/backend/base/langflow/components/composio/slack_composio.py @@ -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", } diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index cf8220738..faa274a1b 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -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): diff --git a/src/backend/base/langflow/graph/vertex/param_handler.py b/src/backend/base/langflow/graph/vertex/param_handler.py index cdaabb3ad..7ed0c1de7 100644 --- a/src/backend/base/langflow/graph/vertex/param_handler.py +++ b/src/backend/base/langflow/graph/vertex/param_handler.py @@ -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: diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index 92cd8841f..d6d1f8339 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -28,6 +28,7 @@ from .inputs import ( StrInput, TabInput, TableInput, + ToolsInput, ) __all__ = [ @@ -62,4 +63,5 @@ __all__ = [ "StrInput", "TabInput", "TableInput", + "ToolsInput", ] diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 2e05590f3..8806336bd 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -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)] diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index e9f3f5cd1..c10171906 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -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 diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py index 86339e7de..f32bc3820 100644 --- a/src/backend/base/langflow/io/__init__.py +++ b/src/backend/base/langflow/io/__init__.py @@ -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", ] diff --git a/src/backend/base/langflow/utils/constants.py b/src/backend/base/langflow/utils/constants.py index 81027b78a..c194b2a67 100644 --- a/src/backend/base/langflow/utils/constants.py +++ b/src/backend/base/langflow/utils/constants.py @@ -69,6 +69,7 @@ DIRECT_TYPES = [ "auth", "connect", "query", + "tools", ] diff --git a/src/backend/tests/unit/base/tools/test_component_toolkit.py b/src/backend/tests/unit/base/tools/test_component_toolkit.py index bf8df7a18..61552a998 100644 --- a/src/backend/tests/unit/base/tools/test_component_toolkit.py +++ b/src/backend/tests/unit/base/tools/test_component_toolkit.py @@ -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"] == { diff --git a/src/backend/tests/unit/base/tools/test_toolmodemixin.py b/src/backend/tests/unit/base/tools/test_toolmodemixin.py index 27518360c..b837a1508 100644 --- a/src/backend/tests/unit/base/tools/test_toolmodemixin.py +++ b/src/backend/tests/unit/base/tools/test_toolmodemixin.py @@ -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"] diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 27f1c1eb7..bb1b03b1e 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -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", diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ComboBoxItem.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ComboBoxItem.tsx new file mode 100644 index 000000000..eb9eac75e --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ComboBoxItem.tsx @@ -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 ( +
+
+ + +
+
+ ); +}; +export default ComboBoxItem; diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx new file mode 100644 index 000000000..720910866 --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/ListItem.tsx @@ -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(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 ( + + ); +}; + +export default ListItem; diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx index 2bef4121b..1f7af104e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx @@ -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(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 ( - - ); -}; - const ListSelectionComponent = ({ open, onClose, @@ -286,7 +173,7 @@ const ListSelectionComponent = ({ name={nodeClass?.icon || "unknown"} className="h-[18px] w-[18px] text-muted-foreground" /> -
+
{nodeClass?.display_name}
diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx index c0ab05610..b7e10f289 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx @@ -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; } diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx new file mode 100644 index 000000000..3a6740d51 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx @@ -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): 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 ( +
+ {value && ( + + )} +
+ {(visibleActions.length > 0 || isAction) && ( + + )} + {!value ? ( +
+ {[...Array(4)].map((_, index) => ( + + ))} +
+ ) : visibleActions.length > 0 ? ( +
+ {visibleActions.map((action, index) => ( + + + {action.name.toUpperCase()} + + + ))} + {remainingCount > 0 && ( + + +{remainingCount} more + + )} +
+ ) : ( + visibleActions.length === 0 && + isAction && ( +
+ + No actions added to this server + + +
+ ) + )} + + {visibleActions.length === 0 && !isAction && value && ( + + )} +
+
+ ); +} diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx index 8d11c292e..c763c7db0 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx @@ -32,27 +32,27 @@ const SortableListItem = memo( }) => (
  • {limit !== 1 && ( )}
    {limit !== 1 && ( -
    +
    {index + 1}
    )} @@ -61,22 +61,16 @@ const SortableListItem = memo(
  • ), diff --git a/src/frontend/src/components/core/parameterRenderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/index.tsx index 45eda0702..b489961d1 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/index.tsx @@ -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 ( + + ); case "slider": return ( void; + open: boolean; + handleOnNewValue: handleOnNewValueType; + isAction: boolean; +}) { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedRows, setSelectedRows] = useState(null); + const agGrid = useRef(null); + + const [focusedRow, setFocusedRow] = useState(null); + const [sidebarName, setSidebarName] = useState(""); + const [sidebarDescription, setSidebarDescription] = useState(""); + + 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 ( + <> +
    +
    + setSearchQuery(e.target.value)} + /> +
    +
    + { + setFocusedRow(event.data); + setSidebarOpen(true); + }} + getRowId={getRowId} + /> +
    +
    + + + {focusedRow && + (isAction ? ( +
    +
    + + + { + setSidebarName(e.target.value); + handleSidebarInputChange("name", e.target.value); + }} + maxLength={46} + placeholder="Edit name..." + data-testid="input_update_name" + /> +
    + {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."} +
    +
    +
    + + +