From 95ceb52fa3ee9d0ad544bbb07e27d9c497cddd01 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:10:48 -0300 Subject: [PATCH] feat: add new Tab input (#7032) * Added TabMixin * Added Tab component on inputs * Added Tab component to initializations * Added tests for tab input * Added Tab Component type * Added options and active tab to input field type * Added tab component on frontend * Instantiate tab component * Update package lock * Refactor input classes and imports for consistency - Reordered imports to maintain consistency across files. - Simplified class definitions by removing unnecessary line breaks. - Updated the `__all__` list in `__init__.py` files to include `TableInput` consistently. - Adjusted test cases for cleaner syntax without altering functionality. * Add constants for tab options limits in input mixin - Introduced `MAX_TAB_OPTIONS` and `MAX_TAB_OPTION_LENGTH` constants for better maintainability. - Updated validation logic in `TabMixin` to use these constants for clearer and more flexible error messages. * Refactor tab input validation tests for improved clarity - Replaced individual test cases for invalid tab inputs with a parameterized test function. - Enhanced test coverage by including cases for too many options, exceeding character limits, and non-string values. - Improved documentation within the test function for better understanding of validation scenarios. * Enhance tab input validation tests with parameterization - Refactored `test_tab_input_valid` to use `pytest.mark.parametrize` for improved test coverage and clarity. - Included multiple scenarios for valid tab inputs, such as standard, fewer options, and empty options. - Updated assertions to reflect the expected outcomes based on parameterized inputs. * Enhance TabInput validation to ensure value is a string and one of the specified options - Updated the `validate_value` method to enforce that the input value is a string. - Added a check to validate that the value is among the allowed options, raising a ValueError with a descriptive message if not. - Improved error handling for better user feedback on invalid inputs. * Fix optional chaining in error handling within CodeAreaModal - Updated the error check in the `delayedFunction` to use optional chaining for safer access to the error detail. - This change ensures that the code handles cases where `detail` may be undefined, improving robustness. * Add 'TAB' field type to schema and update direct types list - Included 'TAB' as a valid field type in the schema conversion dictionary. - Updated the DIRECT_TYPES list to include 'tab', ensuring consistency across type definitions. - These changes enhance the flexibility of the input handling for tab components. * Add unit test * Re-added noqa * fix: unit tests * [autofix.ci] apply automated fixes --------- Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: italojohnny Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/base/langflow/inputs/__init__.py | 2 + src/backend/base/langflow/inputs/constants.py | 2 + .../base/langflow/inputs/input_mixin.py | 27 +++ src/backend/base/langflow/inputs/inputs.py | 57 +++++- src/backend/base/langflow/io/__init__.py | 2 + src/backend/base/langflow/io/schema.py | 1 + src/backend/base/langflow/utils/constants.py | 2 +- src/backend/tests/unit/inputs/test_inputs.py | 81 +++++++++ .../components/tabComponent/index.tsx | 60 +++++++ .../core/parameterRenderComponent/index.tsx | 9 + .../core/parameterRenderComponent/types.ts | 4 + .../src/modals/codeAreaModal/index.tsx | 2 +- src/frontend/src/types/api/index.ts | 2 + .../tests/core/unit/tabComponent.spec.ts | 164 ++++++++++++++++++ 14 files changed, 408 insertions(+), 7 deletions(-) create mode 100644 src/backend/base/langflow/inputs/constants.py create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/tabComponent/index.tsx create mode 100644 src/frontend/tests/core/unit/tabComponent.spec.ts diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index 013e3cdd7..709b03f7c 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -22,6 +22,7 @@ from .inputs import ( SecretStrInput, SliderInput, StrInput, + TabInput, TableInput, ) @@ -52,5 +53,6 @@ __all__ = [ "SecretStrInput", "SliderInput", "StrInput", + "TabInput", "TableInput", ] diff --git a/src/backend/base/langflow/inputs/constants.py b/src/backend/base/langflow/inputs/constants.py new file mode 100644 index 000000000..7b1d048f3 --- /dev/null +++ b/src/backend/base/langflow/inputs/constants.py @@ -0,0 +1,2 @@ +MAX_TAB_OPTIONS = 3 +MAX_TAB_OPTION_LENGTH = 20 diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index d13fc7193..4db153c87 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -11,6 +11,7 @@ from pydantic import ( ) from langflow.field_typing.range_spec import RangeSpec +from langflow.inputs.constants import MAX_TAB_OPTION_LENGTH, MAX_TAB_OPTIONS from langflow.inputs.validators import CoalesceBool from langflow.schema.table import Column, TableOptions, TableSchema @@ -30,6 +31,7 @@ class FieldTypes(str, Enum): TABLE = "table" LINK = "link" SLIDER = "slider" + TAB = "tab" SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)] @@ -188,6 +190,31 @@ class DropDownMixin(BaseModel): """Dictionary of dialog inputs for the field. Default is an empty object.""" +class TabMixin(BaseModel): + """Mixin for tab input fields that allows a maximum of 3 values, each with a maximum of 20 characters.""" + + options: list[str] = Field(default_factory=list, max_length=3) + """List of tab options. Maximum of 3 values allowed.""" + + @field_validator("options") + @classmethod + def validate_options(cls, v): + """Validate that there are at most 3 tab values and each value has at most 20 characters.""" + if len(v) > MAX_TAB_OPTIONS: + msg = f"Maximum of {MAX_TAB_OPTIONS} tab values allowed. Got {len(v)} values." + raise ValueError(msg) + + for i, value in enumerate(v): + if len(value) > MAX_TAB_OPTION_LENGTH: + msg = ( + f"Tab value at index {i} exceeds maximum length of {MAX_TAB_OPTION_LENGTH} " + f"characters. Got {len(value)} characters." + ) + raise ValueError(msg) + + return v + + class MultilineMixin(BaseModel): multiline: CoalesceBool = True diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 13e4f6879..714b6e6f6 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -3,7 +3,7 @@ from collections.abc import AsyncIterator, Iterator from typing import Any, TypeAlias, get_args from pandas import DataFrame -from pydantic import Field, field_validator +from pydantic import Field, field_validator, model_validator from langflow.inputs.validators import CoalesceBool from langflow.schema.data import Data @@ -26,6 +26,7 @@ from .input_mixin import ( SerializableFieldTypes, SliderMixin, TableMixin, + TabMixin, ToolModeMixin, ) @@ -54,7 +55,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi "- A single dictionary (will become a one-row table)\n" "- A Data object (Langflow's internal data structure)\n" ) - raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError + raise ValueError(msg) # noqa: TRY004 # Ensure each item in the list is either a dict or a Data instance. for i, item in enumerate(v): if not isinstance(item, dict | Data): @@ -64,7 +65,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi "- A Data object (Langflow's internal data structure for passing data between components)\n" f"Instead, got a {type(item).__name__}. Please check the format of your input data." ) - raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError + raise ValueError(msg) # noqa: TRY004 return v @@ -105,7 +106,13 @@ class CodeInput(BaseInputMixin, ListableInputMixin, InputTraceMixin, ToolModeMix # Applying mixins to a specific input type -class StrInput(BaseInputMixin, ListableInputMixin, DatabaseLoadMixin, MetadataTraceMixin, ToolModeMixin): +class StrInput( + BaseInputMixin, + ListableInputMixin, + DatabaseLoadMixin, + MetadataTraceMixin, + ToolModeMixin, +): field_type: SerializableFieldTypes = FieldTypes.TEXT load_from_db: CoalesceBool = False """Defines if the field will allow the user to open a text editor. Default is False.""" @@ -399,7 +406,13 @@ class BoolInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolMode value: CoalesceBool = False -class NestedDictInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, InputTraceMixin, ToolModeMixin): +class NestedDictInput( + BaseInputMixin, + ListableInputMixin, + MetadataTraceMixin, + InputTraceMixin, + ToolModeMixin, +): """Represents a nested dictionary field. This class represents a nested dictionary input and provides functionality for handling dictionary values. @@ -451,6 +464,39 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeM dialog_inputs: dict[str, Any] = Field(default_factory=dict) +class TabInput(BaseInputMixin, TabMixin, MetadataTraceMixin, ToolModeMixin): + """Represents a tab input field. + + This class represents a tab input field that allows a maximum of 3 values, each with a maximum of 20 characters. + It inherits from the `BaseInputMixin` and `TabMixin` classes. + + Attributes: + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.TAB. + options (list[str]): List of tab options. Maximum of 3 values allowed, each with a maximum of 20 characters. + active_tab (int): Index of the currently active tab. Defaults to 0. + """ + + field_type: SerializableFieldTypes = FieldTypes.TAB + options: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + @classmethod + def validate_value(cls, values): + """Validates the value to ensure it's one of the tab values.""" + options = values.options # Agora temos certeza de que options está disponível + value = values.value + + if not isinstance(value, str): + msg = f"TabInput value must be a string. Got {type(value).__name__}." + raise TypeError(msg) + + if value not in options and value != "": + msg = f"TabInput value must be one of the following: {options}. Got: '{value}'" + raise ValueError(msg) + + return values + + class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeMixin): """Represents a multiselect input field. @@ -541,6 +587,7 @@ InputTypes: TypeAlias = ( | LinkInput | SliderInput | DataFrameInput + | TabInput ) InputTypesMap: dict[str, type[InputTypes]] = {t.__name__: t for t in get_args(InputTypes)} diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py index 05fbd061e..92bb4c0eb 100644 --- a/src/backend/base/langflow/io/__init__.py +++ b/src/backend/base/langflow/io/__init__.py @@ -22,6 +22,7 @@ from langflow.inputs import ( SecretStrInput, SliderInput, StrInput, + TabInput, TableInput, ) from langflow.template import Output @@ -52,5 +53,6 @@ __all__ = [ "SecretStrInput", "SliderInput", "StrInput", + "TabInput", "TableInput", ] diff --git a/src/backend/base/langflow/io/schema.py b/src/backend/base/langflow/io/schema.py index 3feded3e8..61f6a33a8 100644 --- a/src/backend/base/langflow/io/schema.py +++ b/src/backend/base/langflow/io/schema.py @@ -17,6 +17,7 @@ _convert_field_type_to_type: dict[FieldTypes, type] = { FieldTypes.PROMPT: str, FieldTypes.CODE: str, FieldTypes.OTHER: str, + FieldTypes.TAB: str, } if TYPE_CHECKING: diff --git a/src/backend/base/langflow/utils/constants.py b/src/backend/base/langflow/utils/constants.py index 3e9e3545b..00dd80754 100644 --- a/src/backend/base/langflow/utils/constants.py +++ b/src/backend/base/langflow/utils/constants.py @@ -52,7 +52,7 @@ def python_function(text: str) -> str: PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set] -DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider"] +DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider", "tab"] LOADERS_INFO: list[dict[str, Any]] = [ diff --git a/src/backend/tests/unit/inputs/test_inputs.py b/src/backend/tests/unit/inputs/test_inputs.py index 2a946a2a9..67bbdc6db 100644 --- a/src/backend/tests/unit/inputs/test_inputs.py +++ b/src/backend/tests/unit/inputs/test_inputs.py @@ -19,6 +19,7 @@ from langflow.inputs.inputs import ( SecretStrInput, SliderInput, StrInput, + TabInput, TableInput, ) from langflow.inputs.utils import instantiate_input @@ -213,6 +214,81 @@ def test_file_input_valid(): assert file_input.value == ["/path/to/file"] +@pytest.mark.parametrize( + ("test_id", "options", "value", "expected_options", "expected_value"), + [ + ( + "standard_valid", + ["Tab1", "Tab2", "Tab3"], + "Tab1", + ["Tab1", "Tab2", "Tab3"], + "Tab1", + ), + ( + "fewer_options", + ["Tab1", "Tab2"], + "Tab2", + ["Tab1", "Tab2"], + "Tab2", + ), + ( + "empty_options", + [], + "", + [], + "", + ), + ], +) +def test_tab_input_valid(test_id, options, value, expected_options, expected_value): + """Test TabInput validation with valid inputs.""" + data = TabInput( + name=f"valid_tab_{test_id}", + options=options, + value=value, + ) + assert data.options == expected_options + assert data.value == expected_value + + +@pytest.mark.parametrize( + ("test_id", "options", "value", "error_expected"), + [ + ( + "too_many_options", + ["Tab1", "Tab2", "Tab3", "Tab4"], + "Tab1", + ValidationError, + ), + ( + "option_too_long", + [ + "Tab1", + "ThisTabValueIsTooLongAndExceedsTwentyCharacters", + "Tab3", + ], + "Tab1", + ValidationError, + ), + ( + "non_string_value", + ["Tab1", "Tab2", "Tab3"], + 123, + TypeError, + ), + ], +) +def test_tab_input_invalid(test_id, options, value, error_expected): + """Test TabInput validation with invalid inputs.""" + if error_expected: + with pytest.raises(error_expected): + TabInput( + name=f"invalid_tab_{test_id}", + options=options, + value=value, + ) + + def test_instantiate_input_comprehensive(): valid_data = { "StrInput": {"name": "str_input", "value": "A string"}, @@ -224,6 +300,11 @@ def test_instantiate_input_comprehensive(): "name": "multiselect_input", "value": ["option1", "option2"], }, + "TabInput": { + "name": "tab_input", + "options": ["Tab1", "Tab2", "Tab3"], + "value": "Tab1", + }, } for input_type, data in valid_data.items(): diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/tabComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/tabComponent/index.tsx new file mode 100644 index 000000000..2a9229feb --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/tabComponent/index.tsx @@ -0,0 +1,60 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs-button"; +import { useEffect, useState } from "react"; +import { InputProps, TabComponentType } from "../../types"; + +export default function TabComponent({ + id, + value, + editNode, + handleOnNewValue, + disabled, + options = [], + ...baseInputProps +}: InputProps) { + const [activeTab, setActiveTab] = useState(value || ""); + + // Update the active tab when the component props change + useEffect(() => { + if (options.length > 0) { + // If value is one of the options, use it + if (value && options.includes(value)) { + setActiveTab(value); + } + } + }, [options, value]); + + // Handle tab change + const handleTabChange = (value: string) => { + setActiveTab(value); + handleOnNewValue({ value }, {}); + }; + + // Validate tab values - maximum 3 tabs, each with maximum 20 characters + const validOptions = options + .slice(0, 3) + .map((tab) => (tab.length > 20 ? tab.substring(0, 20) : tab)); + + return ( +
+ + + {validOptions.map((tab, index) => ( + + {tab} + + ))} + + +
+ ); +} diff --git a/src/frontend/src/components/core/parameterRenderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/index.tsx index 1ece294a4..1d63b6a59 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/index.tsx @@ -2,6 +2,7 @@ import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value"; import TableNodeComponent from "@/components/core/parameterRenderComponent/components/TableNodeComponent"; import CodeAreaComponent from "@/components/core/parameterRenderComponent/components/codeAreaComponent"; import SliderComponent from "@/components/core/parameterRenderComponent/components/sliderComponent"; +import TabComponent from "@/components/core/parameterRenderComponent/components/tabComponent"; import { TEXT_FIELD_TYPES } from "@/constants/constants"; import { APIClassType, InputFieldType } from "@/types/api"; import { useMemo } from "react"; @@ -208,6 +209,14 @@ export function ParameterRenderComponent({ id={`slider_${id}`} /> ); + case "tab": + return ( + + ); default: return ; } diff --git a/src/frontend/src/components/core/parameterRenderComponent/types.ts b/src/frontend/src/components/core/parameterRenderComponent/types.ts index f246a5ae5..240ff867d 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/types.ts +++ b/src/frontend/src/components/core/parameterRenderComponent/types.ts @@ -104,6 +104,10 @@ export type MultiselectComponentType = { combobox?: boolean; }; +export type TabComponentType = { + options: string[]; +}; + export type NodeInfoType = { flowId: string; nodeType: string; diff --git a/src/frontend/src/modals/codeAreaModal/index.tsx b/src/frontend/src/modals/codeAreaModal/index.tsx index 5154af10a..eb68ea2ef 100644 --- a/src/frontend/src/modals/codeAreaModal/index.tsx +++ b/src/frontend/src/modals/codeAreaModal/index.tsx @@ -145,7 +145,7 @@ export default function CodeAreaModal({ useEffect(() => { // Function to be executed after the state changes const delayedFunction = setTimeout(() => { - if (error?.detail.error !== undefined) { + if (error?.detail?.error !== undefined) { //trigger to update the height, does not really apply any height setHeight("90%"); } diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index d5cc6c03c..921cc5279 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -85,6 +85,8 @@ export type InputFieldType = { refresh_button_text?: string; combobox?: boolean; info?: string; + options?: string[]; + active_tab?: number; [key: string]: any; icon?: string; text?: string; diff --git a/src/frontend/tests/core/unit/tabComponent.spec.ts b/src/frontend/tests/core/unit/tabComponent.spec.ts new file mode 100644 index 000000000..847282e6c --- /dev/null +++ b/src/frontend/tests/core/unit/tabComponent.spec.ts @@ -0,0 +1,164 @@ +import { expect, Page, test } from "@playwright/test"; +import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; + +test( + "user should interact with tab component", + { tag: ["@release", "@workspace"] }, + async ({ context, page }) => { + await awaitBootstrapTest(page); + + await page.waitForSelector('[data-testid="blank-flow"]', { + timeout: 30000, + }); + await page.getByTestId("blank-flow").click(); + + await page.waitForSelector( + '[data-testid="sidebar-custom-component-button"]', + { + timeout: 3000, + }, + ); + + await page.getByTestId("sidebar-custom-component-button").click(); + await page.getByTitle("fit view").click(); + await page.getByTitle("zoom out").click(); + + await page.getByTestId("title-Custom Component").first().click(); + + await page.waitForSelector('[data-testid="code-button-modal"]', { + timeout: 3000, + }); + + await page.getByTestId("code-button-modal").click(); + + let cleanCode = await extractAndCleanCode(page); + + // Use regex pattern to match the imports section more flexibly + cleanCode = updateComponentCode(cleanCode, { + imports: ["MessageTextInput", "Output", "TabInput"], + inputs: [ + { + name: "MessageTextInput", + config: { + name: "input_value", + display_name: "Input Value", + info: "This is a custom component Input", + value: "Hello, World!", + tool_mode: true, + }, + }, + { + name: "TabInput", + config: { + name: "tab_selection", + display_name: "Tab Selection", + options: ["Tab 1", "Tab 2", "Tab 3"], + value: "Tab 1", + }, + }, + ], + }); + + await page.locator("textarea").last().press(`ControlOrMeta+a`); + await page.keyboard.press("Backspace"); + await page.locator("textarea").last().fill(cleanCode); + await page.locator('//*[@id="checkAndSaveBtn"]').click(); + + await page.waitForSelector('[data-testid="fit_view"]', { + timeout: 3000, + }); + + await page.getByTestId("fit_view").click(); + await page.getByTestId("zoom_out").click(); + + // Verify that all tabs are visible + expect(await page.getByText("Tab 1").isVisible()).toBeTruthy(); + expect(await page.getByText("Tab 2").isVisible()).toBeTruthy(); + expect(await page.getByText("Tab 3").isVisible()).toBeTruthy(); + + // Verify that Tab 1 is active by default (as specified in the value) + expect( + await page + .getByRole("tab", { name: "Tab 1", selected: true }) + .isVisible(), + ).toBeTruthy(); + + // Click on Tab 2 and verify it becomes active + await page.getByRole("tab", { name: "Tab 2" }).click(); + expect( + await page + .getByRole("tab", { name: "Tab 2", selected: true }) + .isVisible(), + ).toBeTruthy(); + + // Click on Tab 3 and verify it becomes active + await page.getByRole("tab", { name: "Tab 3" }).click(); + expect( + await page + .getByRole("tab", { name: "Tab 3", selected: true }) + .isVisible(), + ).toBeTruthy(); + }, +); + +async function extractAndCleanCode(page: Page): Promise { + const outerHTML = await page + .locator('//*[@id="codeValue"]') + .evaluate((el) => el.outerHTML); + + const valueMatch = outerHTML.match(/value="([\s\S]*?)"/); + if (!valueMatch) { + throw new Error("Could not find value attribute in the HTML"); + } + + let codeContent = valueMatch[1] + .replace(/"/g, '"') + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/'/g, "'") + .replace(///g, "/"); + + return codeContent; +} + +function updateComponentCode( + code: string, + updates: { + imports?: string[]; + inputs?: Array<{ name: string; config: Record }>; + }, +): string { + let updatedCode = code; + + // Update imports + if (updates.imports) { + const importPattern = /from\s+langflow\.io\s+import\s+([^;\n]+)/; + const newImports = updates.imports.join(", "); + updatedCode = updatedCode.replace( + importPattern, + `from langflow.io import ${newImports}`, + ); + } + + // Update inputs + if (updates.inputs) { + const inputsPattern = /inputs\s*=\s*\[([\s\S]*?)\]/; + const newInputs = updates.inputs + .map(({ name, config }) => { + const params = Object.entries(config) + .map(([key, value]) => `${key}=${JSON.stringify(value)}`) + .join(",\n "); + return ` ${name}(\n ${params}\n )`; + }) + .join(",\n"); + updatedCode = updatedCode.replace( + inputsPattern, + `inputs = [\n${newInputs}\n ]`, + ); + updatedCode = updatedCode.replace("true", "True"); + updatedCode = updatedCode.replace("false", "False"); + } + + return updatedCode; +}