feat: Quality of Life improvements for Astra Component (#6953)

* feat: Add helper text for Astra DB embedding generation provider

* feat: Add helper text support for dropdown and parameter components

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update astradb.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* feat: Add org_id to excluded metadata keys in dropdown component

* fix: Update Astra DB component UI and starter project configuration

- Improved AstraDB component's embedding provider link with better accessibility attributes
- Updated Vector Store RAG starter project JSON with formatted configuration
- Refined UI text and link presentation for better user experience

* Remove providers that arent configured

* feat: Add required field indicator to node parameters

- Implemented visual indicator (*) for required node parameters
- Updated custom parameter title rendering to show required status
- Enhanced UI to provide clearer input requirements

* Revert "Merge branch 'lfoss-683-1' of https://github.com/langflow-ai/langflow into lfoss-683-1"

This reverts commit 2eb0fcb4d3d347de0df1f021f761ea63e882b757, reversing
changes made to 9ea44aca477e8a418ef82722da4b1e0fc2afa2e1.

* [autofix.ci] apply automated fixes

* Add changes back

* Fix some comments

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Update astradb.py

* feat: Add dynamic disabled state for node dialog and int component

* feat: Add dynamic placeholder for node dialog input

* [autofix.ci] apply automated fixes

* feat: Enhance IntComponent with disabled state styling and stepper interactions

* Fix some db creation params

* Update astradb.py

* fix: Resolve node dialog input value handling in TypeScript

* Update astradb.py

* feat: Add required field validation to node dialog submission

* Update astradb.py

* [autofix.ci] apply automated fixes

* Update astradb.py

* Clean up functions and add read only flag

* feat: Add readonly support to IntComponent and improve node dialog validation

* fix: Improve dropdown component disabled and empty options handling

* console.log removed

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Read only field and cleanup

* Astra DB selector updates

* [autofix.ci] apply automated fixes

* fix: Improve dropdown component disabled state handling

* Fix value of region in create db

* [autofix.ci] apply automated fixes

* Remove database icon from component

* fix: Conditionally render dropdown option icons

* Don't show Nvidia if not available

* [autofix.ci] apply automated fixes

* Exceptions unless pending mean, skip

* feat: Enhance AstraDBVectorStoreComponent with HTML sanitization

- Added DOMPurify for sanitizing HTML input in the convertStringToHTML function.
- Introduced a new sanitizeHTML function to ensure safe rendering of HTML strings.

* fix: Correct field key usage in NodeDialog and refactor disabled class name in IntComponent

- Updated NodeDialogComponent to use fieldKey instead of underscore for better clarity in mapping field values.
- Refactored IntComponent to replace the getDisabledClassName function with a constant for the disabled input class, improving readability.

* ✨ (playground.spec.ts): Refactor drag and drop functionality to use hover and click for better interaction
🔧 (chatInputOutput.spec.ts): Update drag and drop functionality to use target position for more precise placement
🔧 (chatInputOutputUser-shard-1.spec.ts): Simplify connecting elements by replacing hover and mouse actions with click for better user experience

* 🐛 (generalBugs-prompt.spec.ts): fix issue with selecting the last element with getByTestId to ensure correct element is clicked and tested

* ✅ (chatInputOutputUser-shard-1.spec.ts): add 600ms timeout to wait for elements to be interactable before proceeding with the test steps

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Deon Sanchez 2025-03-14 16:34:14 -06:00 • committed by GitHub
commit 4ab4e736be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1443 additions and 2146 deletions

View file

@ -1,3 +1,4 @@
import re
from collections import defaultdict from collections import defaultdict
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
@ -97,6 +98,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
name="embedding_generation_provider", name="embedding_generation_provider",
display_name="Embedding generation method", display_name="Embedding generation method",
info="Provider to use for generating embeddings.", info="Provider to use for generating embeddings.",
helper_text=(
"To create collections with more embedding provider options, go to "
'<a class="underline" href="https://astra.datastax.com/" target=" _blank" '
'rel="noopener noreferrer">your database in Astra DB</a>'
),
real_time_refresh=True, real_time_refresh=True,
required=True, required=True,
options=[], options=[],
@ -105,15 +111,14 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
name="embedding_generation_model", name="embedding_generation_model",
display_name="Embedding model", display_name="Embedding model",
info="Model to use for generating embeddings.", info="Model to use for generating embeddings.",
required=True, real_time_refresh=True,
options=[], options=[],
), ),
"04_dimension": IntInput( "04_dimension": IntInput(
name="dimension", name="dimension",
display_name="Dimensions (Required only for `Bring your own`)", display_name="Dimensions",
info="Dimensions of the embeddings to generate.", info="Dimensions of the embeddings to generate.",
required=False, value=None,
value=1024,
), ),
}, },
}, },
@ -307,9 +312,8 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
# Sort the resulting dictionary # Sort the resulting dictionary
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items()))) return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
except Exception as e: except Exception as _: # noqa: BLE001
msg = f"Error fetching vectorize providers: {e}" return {}
raise ValueError(msg) from e
@classmethod @classmethod
async def create_database_api( async def create_database_api(
@ -329,6 +333,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
# Get the environment, set to prod if null like # Get the environment, set to prod if null like
my_env = environment or "prod" my_env = environment or "prod"
# Raise a value error if name isn't provided
if not new_database_name:
msg = "Database name is required to create a new database."
raise ValueError(msg)
# Call the create database function # Call the create database function
return await admin_client.async_create_database( return await admin_client.async_create_database(
name=new_database_name, name=new_database_name,
@ -366,6 +375,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
model_name=embedding_generation_model, model_name=embedding_generation_model,
) )
# Raise a value error if name isn't provided
if not new_collection_name:
msg = "Collection name is required to create a new collection."
raise ValueError(msg)
# Create the collection # Create the collection
return await database.create_collection( return await database.create_collection(
name=new_collection_name, name=new_collection_name,
@ -406,15 +420,16 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
) )
) )
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
num_collections = 0
if db.status != "PENDING": if db.status != "PENDING":
continue continue
num_collections = 0
# Add the database to the dictionary # Add the database to the dictionary
db_info_dict[db.info.name] = { db_info_dict[db.info.name] = {
"api_endpoint": api_endpoint, "api_endpoint": api_endpoint,
"collections": num_collections, "collections": num_collections,
"status": db.status if db.status != "ACTIVE" else None, "status": db.status if db.status != "ACTIVE" else None,
"org_id": db.org_id if db.org_id else None,
} }
except Exception: # noqa: BLE001, S110 except Exception: # noqa: BLE001, S110
pass pass
@ -460,6 +475,17 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
database_name=self.database_name, database_name=self.database_name,
) )
@classmethod
def get_database_id_static(cls, api_endpoint: str) -> str | None:
# Pattern matches standard UUID format: 8-4-4-4-12 hexadecimal characters
uuid_pattern = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
match = re.search(uuid_pattern, api_endpoint)
return match.group(0) if match else None
def get_database_id(self):
return self.get_database_id_static(api_endpoint=self.get_api_endpoint())
def get_keyspace(self): def get_keyspace(self):
keyspace = self.keyspace keyspace = self.keyspace
@ -508,7 +534,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
"status": info["status"], "status": info["status"],
"collections": info["collections"], "collections": info["collections"],
"api_endpoint": info["api_endpoint"], "api_endpoint": info["api_endpoint"],
"icon": "data", "org_id": info["org_id"],
} }
for name, info in self.get_database_list().items() for name, info in self.get_database_list().items()
] ]
@ -573,123 +599,193 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
for col in collection_list for col in collection_list
] ]
def reset_provider_options(self, build_config: dict): def reset_provider_options(self, build_config: dict) -> dict:
# Get the list of vectorize providers """Reset provider options and related configurations in the build_config dictionary."""
vectorize_providers = self.get_vectorize_providers( # Extract template path for cleaner access
template = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
# Get vectorize providers
vectorize_providers_api = self.get_vectorize_providers(
token=self.token, token=self.token,
environment=self.environment, environment=self.environment,
api_endpoint=build_config["api_endpoint"]["value"], api_endpoint=build_config["api_endpoint"]["value"],
) )
# Append a special case for Bring your own # Create a new dictionary with "Bring your own" first
vectorize_providers["Bring your own"] = [None, ["Bring your own"]] vectorize_providers: dict[str, list[list[str]]] = {"Bring your own": [[], []]}
# If the collection is set, allow user to see embedding options # Add the remaining items (only Nvidia) from the original dictionary
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][ vectorize_providers.update(
"02_embedding_generation_provider" {
]["options"] = [ k: v
"Bring your own", for k, v in vectorize_providers_api.items()
"Nvidia", if k.lower() in ["nvidia"] # TODO: Eventually support more
*[key for key in vectorize_providers if key not in ["Bring your own", "Nvidia"]], }
)
# Set provider options
provider_field = "02_embedding_generation_provider"
template[provider_field]["options"] = list(vectorize_providers.keys())
# Add metadata for each provider option
template[provider_field]["options_metadata"] = [
{"icon": self.get_provider_icon(provider_name=provider)} for provider in template[provider_field]["options"]
] ]
# For all not Bring your own or Nvidia providers, add metadata saying configure in Astra DB Portal # Get selected embedding provider
provider_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][ embedding_provider = template[provider_field]["value"]
"02_embedding_generation_provider" is_bring_your_own = embedding_provider and embedding_provider == "Bring your own"
]["options"]
# Go over each possible provider and add metadata to configure in Astra DB Portal # Configure embedding model field
for provider in provider_options: model_field = "03_embedding_generation_model"
# Add the icon for the provider template[model_field].update(
my_metadata = {"icon": self.get_provider_icon(provider_name=provider)} {
"options": vectorize_providers.get(embedding_provider, [[], []])[1],
"placeholder": "Bring your own" if is_bring_your_own else None,
"readonly": is_bring_your_own,
"required": not is_bring_your_own,
"value": None,
}
)
# Skip Bring your own and Nvidia, automatically configured # If this is a bring your own, set dimensions to 0
if provider not in {"Bring your own", "Nvidia"}: return self.reset_dimension_field(build_config)
# Add metadata to configure in Astra DB Portal
my_metadata[" "] = "Configure in Astra DB Portal"
# Add the metadata to the options metadata def reset_dimension_field(self, build_config: dict) -> dict:
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][ """Reset dimension field options based on provided configuration."""
"02_embedding_generation_provider" # Extract template path for cleaner access
]["options_metadata"].append(my_metadata) template = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
# And allow the user to see the models based on a selected provider # Get selected embedding model
embedding_provider = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][ provider_field = "02_embedding_generation_provider"
"02_embedding_generation_provider" embedding_provider = template[provider_field]["value"]
]["value"] is_bring_your_own = embedding_provider and embedding_provider == "Bring your own"
# Set the options for the embedding model based on the provider # Configure dimension field
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][ dimension_field = "04_dimension"
"03_embedding_generation_model" dimension_value = 1024 if not is_bring_your_own else None # TODO: Dynamically figure this out
]["options"] = vectorize_providers.get(embedding_provider, [[], []])[1] template[dimension_field].update(
{
"placeholder": dimension_value,
"value": dimension_value,
"readonly": not is_bring_your_own,
"required": is_bring_your_own,
}
)
return build_config return build_config
def reset_collection_list(self, build_config: dict): def reset_collection_list(self, build_config: dict) -> dict:
# Get the list of options we have based on the token provided """Reset collection list options based on provided configuration."""
# Get collection options
collection_options = self._initialize_collection_options(api_endpoint=build_config["api_endpoint"]["value"]) collection_options = self._initialize_collection_options(api_endpoint=build_config["api_endpoint"]["value"])
# If we retrieved options based on the token, show the dropdown # Update collection configuration
build_config["collection_name"]["options"] = [col["name"] for col in collection_options] collection_config = build_config["collection_name"]
build_config["collection_name"]["options_metadata"] = [ collection_config.update(
{k: v for k, v in col.items() if k != "name"} for col in collection_options {
] "options": [col["name"] for col in collection_options],
"options_metadata": [{k: v for k, v in col.items() if k != "name"} for col in collection_options],
}
)
# Reset the selected collection # Reset selected collection if not in options
if build_config["collection_name"]["value"] not in build_config["collection_name"]["options"]: if collection_config["value"] not in collection_config["options"]:
build_config["collection_name"]["value"] = "" collection_config["value"] = ""
# If we have a database, collection name should not be advanced # Set advanced status based on database selection
build_config["collection_name"]["advanced"] = not build_config["database_name"]["value"] collection_config["advanced"] = not build_config["database_name"]["value"]
return build_config return build_config
def reset_database_list(self, build_config: dict): def reset_database_list(self, build_config: dict) -> dict:
# Get the list of options we have based on the token provided """Reset database list options and related configurations."""
# Get database options
database_options = self._initialize_database_options() database_options = self._initialize_database_options()
# Update the list of cloud providers # Update cloud provider options
my_env = self.environment or "prod" env = self.environment or "prod"
build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["02_cloud_provider"][ template = build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
"options" template["02_cloud_provider"]["options"] = list(self.map_cloud_providers()[env].keys())
] = list(self.map_cloud_providers()[my_env].keys())
# If we retrieved options based on the token, show the dropdown # Update database configuration
build_config["database_name"]["options"] = [db["name"] for db in database_options] database_config = build_config["database_name"]
build_config["database_name"]["options_metadata"] = [ database_config.update(
{k: v for k, v in db.items() if k != "name"} for db in database_options {
] "options": [db["name"] for db in database_options],
"options_metadata": [{k: v for k, v in db.items() if k != "name"} for db in database_options],
}
)
# Reset the selected database # Reset selections if value not in options
if build_config["database_name"]["value"] not in build_config["database_name"]["options"]: if database_config["value"] not in database_config["options"]:
build_config["database_name"]["value"] = "" database_config["value"] = ""
build_config["api_endpoint"]["value"] = "" build_config["api_endpoint"]["value"] = ""
build_config["collection_name"]["advanced"] = True build_config["collection_name"]["advanced"] = True
# If we have a token, database name should not be advanced # Set advanced status based on token presence
build_config["database_name"]["advanced"] = not build_config["token"]["value"] database_config["advanced"] = not build_config["token"]["value"]
return build_config return build_config
def reset_build_config(self, build_config: dict): def reset_build_config(self, build_config: dict) -> dict:
# Reset the list of databases we have based on the token provided """Reset all build configuration options to default empty state."""
build_config["database_name"]["options"] = [] # Reset database configuration
build_config["database_name"]["options_metadata"] = [] database_config = build_config["database_name"]
build_config["database_name"]["value"] = "" database_config.update({"options": [], "options_metadata": [], "value": "", "advanced": True})
build_config["database_name"]["advanced"] = True
build_config["api_endpoint"]["value"] = "" build_config["api_endpoint"]["value"] = ""
# Reset the list of collections and metadata associated # Reset collection configuration
build_config["collection_name"]["options"] = [] collection_config = build_config["collection_name"]
build_config["collection_name"]["options_metadata"] = [] collection_config.update({"options": [], "options_metadata": [], "value": "", "advanced": True})
build_config["collection_name"]["value"] = ""
build_config["collection_name"]["advanced"] = True
return build_config return build_config
async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None): async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:
# Callback for database creation """Update build configuration based on field name and value."""
if field_name == "database_name" and isinstance(field_value, dict) and "01_new_database_name" in field_value: # Early return if no token provided
if not self.token:
return self.reset_build_config(build_config)
# Database creation callback
if field_name == "database_name" and isinstance(field_value, dict):
if "01_new_database_name" in field_value:
await self._create_new_database(build_config, field_value)
return self.reset_collection_list(build_config)
return self._update_cloud_regions(build_config, field_value)
# Collection creation callback
if field_name == "collection_name" and isinstance(field_value, dict):
# Case 1: New collection creation
if "01_new_collection_name" in field_value:
await self._create_new_collection(build_config, field_value)
return build_config
# Case 2: Update embedding provider options
if "02_embedding_generation_provider" in field_value:
return self.reset_provider_options(build_config)
# Case 3: Update dimension field
if "03_embedding_generation_model" in field_value:
return self.reset_dimension_field(build_config)
# Initial execution or token/environment change
first_run = field_name == "collection_name" and not field_value and not build_config["database_name"]["options"]
if first_run or field_name in {"token", "environment"}:
return self.reset_database_list(build_config)
# Database selection change
if field_name == "database_name" and not isinstance(field_value, dict):
return self._handle_database_selection(build_config, field_value)
# Collection selection change
if field_name == "collection_name" and not isinstance(field_value, dict):
return self._handle_collection_selection(build_config, field_value)
return build_config
async def _create_new_database(self, build_config: dict, field_value: dict) -> None:
"""Create a new database and update build config options."""
try: try:
await self.create_database_api( await self.create_database_api(
new_database_name=field_value["01_new_database_name"], new_database_name=field_value["01_new_database_name"],
@ -703,138 +799,109 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
msg = f"Error creating database: {e}" msg = f"Error creating database: {e}"
raise ValueError(msg) from e raise ValueError(msg) from e
# Add the new database to the list of options build_config["database_name"]["options"].append(field_value["01_new_database_name"])
build_config["database_name"]["options"] += [field_value["01_new_database_name"]] build_config["database_name"]["options_metadata"].append(
build_config["database_name"]["options_metadata"] += [{"status": "PENDING"}] {
"status": "PENDING",
"collections": 0,
"api_endpoint": None,
"org_id": None,
}
)
return self.reset_collection_list(build_config) def _update_cloud_regions(self, build_config: dict, field_value: dict) -> dict:
"""Update cloud provider regions in build config."""
# This is the callback required to update the list of regions for a cloud provider env = self.environment or "prod"
if (
field_name == "database_name"
and isinstance(field_value, dict)
and "01_new_database_name" not in field_value
):
# Get the cloud provider and environment
cloud_provider = field_value["02_cloud_provider"] cloud_provider = field_value["02_cloud_provider"]
my_env = self.environment or "prod"
# Update the list of regions for the cloud provider # Update the region options based on the selected cloud provider
build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["03_region"][ template = build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
"options" template["03_region"]["options"] = self.map_cloud_providers()[env][cloud_provider]["regions"]
] = self.map_cloud_providers()[my_env][cloud_provider]["regions"]
# Reset the the 03_region value if it's not in the new options
if template["03_region"]["value"] not in template["03_region"]["options"]:
template["03_region"]["value"] = None
return build_config return build_config
# Callback for the creation of collections async def _create_new_collection(self, build_config: dict, field_value: dict) -> None:
if ( """Create a new collection and update build config options."""
field_name == "collection_name" embedding_provider = field_value.get("02_embedding_generation_provider")
and isinstance(field_value, dict)
and "01_new_collection_name" in field_value
):
try: try:
# Get the dimension if its a BYO provider
dimension = (
field_value["04_dimension"]
if field_value["02_embedding_generation_provider"] == "Bring your own"
else None
)
# Create the collection
await self.create_collection_api( await self.create_collection_api(
new_collection_name=field_value["01_new_collection_name"], new_collection_name=field_value["01_new_collection_name"],
token=self.token, token=self.token,
api_endpoint=build_config["api_endpoint"]["value"], api_endpoint=build_config["api_endpoint"]["value"],
environment=self.environment, environment=self.environment,
keyspace=self.get_keyspace(), keyspace=self.get_keyspace(),
dimension=dimension, dimension=field_value.get("04_dimension") if embedding_provider == "Bring your own" else None,
embedding_generation_provider=field_value["02_embedding_generation_provider"], embedding_generation_provider=embedding_provider,
embedding_generation_model=field_value["03_embedding_generation_model"], embedding_generation_model=field_value.get("03_embedding_generation_model"),
) )
except Exception as e: except Exception as e:
msg = f"Error creating collection: {e}" msg = f"Error creating collection: {e}"
raise ValueError(msg) from e raise ValueError(msg) from e
# Add the new collection to the list of options provider = embedding_provider.lower() if embedding_provider and embedding_provider != "Bring your own" else None
build_config["collection_name"]["value"] = field_value["01_new_collection_name"] build_config["collection_name"].update(
build_config["collection_name"]["options"].append(field_value["01_new_collection_name"]) {
"value": field_value["01_new_collection_name"],
# Get the provider and model for the new collection "options": build_config["collection_name"]["options"] + [field_value["01_new_collection_name"]],
generation_provider = field_value["02_embedding_generation_provider"] }
provider = (
generation_provider.lower() if generation_provider and generation_provider != "Bring your own" else None
) )
generation_model = field_value["03_embedding_generation_model"]
model = generation_model if generation_model and generation_model != "Bring your own" else None
# Set the embedding choice
build_config["embedding_choice"]["value"] = "Astra Vectorize" if provider else "Embedding Model" build_config["embedding_choice"]["value"] = "Astra Vectorize" if provider else "Embedding Model"
build_config["embedding_model"]["advanced"] = bool(provider) build_config["embedding_model"]["advanced"] = bool(provider)
build_config["collection_name"]["options_metadata"].append(
{
"records": 0,
"provider": provider,
"icon": self.get_provider_icon(provider_name=embedding_provider),
"model": field_value.get("03_embedding_generation_model"),
}
)
# Add the new collection to the list of options def _handle_database_selection(self, build_config: dict, field_value: str) -> dict:
icon = self.get_provider_icon(provider_name=generation_provider) """Handle database selection and update related configurations."""
build_config["collection_name"]["options_metadata"] += [ build_config = self.reset_database_list(build_config)
{"records": 0, "provider": provider, "icon": icon, "model": model}
]
# Reset collection list if database selection changes
if field_value not in build_config["database_name"]["options"]:
build_config["database_name"]["value"] = ""
return build_config return build_config
# Callback to update the model list based on the embedding provider # Get the api endpoint for the selected database
if ( index = build_config["database_name"]["options"].index(field_value)
field_name == "collection_name" build_config["api_endpoint"]["value"] = build_config["database_name"]["options_metadata"][index]["api_endpoint"]
and isinstance(field_value, dict)
and "01_new_collection_name" not in field_value
):
return self.reset_provider_options(build_config)
# When the component first executes, this is the update refresh call # Get the org_id for the selected database
first_run = field_name == "collection_name" and not field_value and not build_config["database_name"]["options"] org_id = build_config["database_name"]["options_metadata"][index]["org_id"]
if not org_id:
return build_config
# If the token has not been provided, simply return the empty build config # Get the database id for the selected database
if not self.token: db_id = self.get_database_id_static(api_endpoint=build_config["api_endpoint"]["value"])
return self.reset_build_config(build_config) keyspace = self.get_keyspace() or "default_keyspace"
# If this is the first execution of the component, reset and build database list # Update the helper text for the embedding provider field
if first_run or field_name in {"token", "environment"}: template = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
return self.reset_database_list(build_config) template["02_embedding_generation_provider"]["helper_text"] = (
"To create collections with more embedding provider options, go to "
f'<a class="underline" target="_blank" rel="noopener noreferrer" '
f'href="https://astra.datastax.com/org/{org_id}/database/{db_id}/data-explorer?createCollection=1&namespace={keyspace}">'
"your database in Astra DB</a>."
)
# Refresh the collection name options # Reset provider options
if field_name == "database_name" and not isinstance(field_value, dict):
# If missing, refresh the database options
if field_value not in build_config["database_name"]["options"]:
build_config = await self.update_build_config(build_config, field_value=self.token, field_name="token")
build_config["database_name"]["value"] = ""
else:
# Find the position of the selected database to align with metadata
index_of_name = build_config["database_name"]["options"].index(field_value)
# Initializing database condition
pending = build_config["database_name"]["options_metadata"][index_of_name]["status"] == "PENDING"
if pending:
return self.update_build_config(build_config, field_value=self.token, field_name="token")
# Set the API endpoint based on the selected database
build_config["api_endpoint"]["value"] = build_config["database_name"]["options_metadata"][
index_of_name
]["api_endpoint"]
# Reset the provider options
build_config = self.reset_provider_options(build_config) build_config = self.reset_provider_options(build_config)
# Reset the list of collections we have based on the token provided
return self.reset_collection_list(build_config) return self.reset_collection_list(build_config)
# Hide embedding model option if opriona_metadata provider is not null def _handle_collection_selection(self, build_config: dict, field_value: str) -> dict:
if field_name == "collection_name" and not isinstance(field_value, dict): """Handle collection selection and update embedding options."""
# Assume we will be autodetecting the collection:
build_config["autodetect_collection"]["value"] = True build_config["autodetect_collection"]["value"] = True
# Reload the collection list
build_config = self.reset_collection_list(build_config) build_config = self.reset_collection_list(build_config)
# Set the options for collection name to be the field value if its a new collection
if field_value and field_value not in build_config["collection_name"]["options"]: if field_value and field_value not in build_config["collection_name"]["options"]:
# Add the new collection to the list of options
build_config["collection_name"]["options"].append(field_value) build_config["collection_name"]["options"].append(field_value)
build_config["collection_name"]["options_metadata"].append( build_config["collection_name"]["options_metadata"].append(
{ {
@ -844,24 +911,15 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
"model": None, "model": None,
} }
) )
# Ensure that autodetect collection is set to False, since its a new collection
build_config["autodetect_collection"]["value"] = False build_config["autodetect_collection"]["value"] = False
# If nothing is selected, can't detect provider - return
if not field_value: if not field_value:
return build_config return build_config
# Find the position of the selected collection to align with metadata index = build_config["collection_name"]["options"].index(field_value)
index_of_name = build_config["collection_name"]["options"].index(field_value) provider = build_config["collection_name"]["options_metadata"][index]["provider"]
value_of_provider = build_config["collection_name"]["options_metadata"][index_of_name]["provider"] build_config["embedding_model"]["advanced"] = bool(provider)
build_config["embedding_choice"]["value"] = "Astra Vectorize" if provider else "Embedding Model"
# If we were able to determine the Vectorize provider, set it accordingly
build_config["embedding_model"]["advanced"] = bool(value_of_provider)
build_config["embedding_choice"]["value"] = "Astra Vectorize" if value_of_provider else "Embedding Model"
return build_config
return build_config return build_config
@check_cached_vector_store @check_cached_vector_store

File diff suppressed because one or more lines are too long

View file

@ -72,6 +72,9 @@ class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore[call-
dynamic: bool = False dynamic: bool = False
"""Specifies if the field is dynamic. Defaults to False.""" """Specifies if the field is dynamic. Defaults to False."""
helper_text: str | None = None
"""Adds a helper text to the field. Defaults to an empty string."""
info: str | None = "" info: str | None = ""
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string.""" """Additional information about the field to be shown in the tooltip. Defaults to an empty string."""

File diff suppressed because it is too large Load diff

View file

@ -110,6 +110,28 @@ export const NodeDialog: React.FC<NodeDialogProps> = ({
}; };
const handleSubmitDialog = async () => { const handleSubmitDialog = async () => {
// Validate required fields first
const missingRequiredFields = Object.entries(dialogTemplate)
.filter(
([key, fieldValue]) =>
(fieldValue as { required: boolean })?.required === true &&
(!fieldValues[key] ||
(typeof fieldValues[key] === "string" &&
fieldValues[key].trim() === "")),
)
.map(
([fieldKey, fieldValue]) =>
(fieldValue as { display_name: string })?.display_name || fieldKey,
);
if (missingRequiredFields.length > 0) {
handleErrorData({
title: "Missing required fields",
list: missingRequiredFields,
});
return;
}
setIsLoading(true); setIsLoading(true);
await mutateTemplate( await mutateTemplate(
@ -162,13 +184,15 @@ export const NodeDialog: React.FC<NodeDialogProps> = ({
<div className="flex flex-col gap-5 overflow-y-auto px-5"> <div className="flex flex-col gap-5 overflow-y-auto px-5">
{Object.entries(dialogTemplate).map(([fieldKey, fieldValue]) => ( {Object.entries(dialogTemplate).map(([fieldKey, fieldValue]) => (
<div key={fieldKey}> <div key={fieldKey}>
<div> <div className="flex items-center gap-2">
{getCustomParameterTitle({ {getCustomParameterTitle({
title: title:
(fieldValue as { display_name: string })?.display_name ?? (fieldValue as { display_name: string })?.display_name ??
"", "",
nodeId, nodeId,
isFlexView: false, isFlexView: false,
required:
(fieldValue as { required: boolean })?.required ?? false,
})} })}
</div> </div>
<ParameterRenderComponent <ParameterRenderComponent
@ -178,12 +202,16 @@ export const NodeDialog: React.FC<NodeDialogProps> = ({
name={fieldKey} name={fieldKey}
nodeId={nodeId} nodeId={nodeId}
templateData={fieldValue as Partial<InputFieldType>} templateData={fieldValue as Partial<InputFieldType>}
templateValue={fieldValues[fieldKey] || ""} templateValue={(fieldValue as { value: string })?.value ?? ""}
editNode={false} editNode={false}
handleNodeClass={() => {}} handleNodeClass={() => {}}
nodeClass={dialogNodeData} nodeClass={dialogNodeData}
disabled={false} disabled={
placeholder="" (fieldValue as { disabled: boolean })?.disabled ?? false
}
placeholder={
(fieldValue as { placeholder: string })?.placeholder ?? ""
}
isToolMode={false} isToolMode={false}
/> />
</div> </div>

View file

@ -3,7 +3,10 @@ import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-t
import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent"; import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent";
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template"; import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
import useAlertStore from "@/stores/alertStore"; import useAlertStore from "@/stores/alertStore";
import { getStatusColor } from "@/utils/stringManipulation"; import {
convertStringToHTML,
getStatusColor,
} from "@/utils/stringManipulation";
import { PopoverAnchor } from "@radix-ui/react-popover"; import { PopoverAnchor } from "@radix-ui/react-popover";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
@ -66,7 +69,8 @@ export default function Dropdown({
const fuse = new Fuse(validOptions, { keys: ["name", "value"] }); const fuse = new Fuse(validOptions, { keys: ["name", "value"] });
const PopoverContentDropdown = const PopoverContentDropdown =
children || editNode ? PopoverContent : PopoverContentWithoutPortal; children || editNode ? PopoverContent : PopoverContentWithoutPortal;
const { nodeClass, nodeId, handleNodeClass, tooltip } = baseInputProps; const { nodeClass, nodeId, handleNodeClass, tooltip, helperText } =
baseInputProps;
// API and store hooks // API and store hooks
const postTemplateValue = usePostTemplateValue({ const postTemplateValue = usePostTemplateValue({
@ -79,7 +83,7 @@ export default function Dropdown({
// Utility functions // Utility functions
const filterMetadataKeys = ( const filterMetadataKeys = (
metadata: Record<string, any> = {}, metadata: Record<string, any> = {},
excludeKeys: string[] = ["api_endpoint", "icon", "status"], excludeKeys: string[] = ["api_endpoint", "icon", "status", "org_id"],
) => { ) => {
return Object.fromEntries( return Object.fromEntries(
Object.entries(metadata).filter(([key]) => !excludeKeys.includes(key)), Object.entries(metadata).filter(([key]) => !excludeKeys.includes(key)),
@ -161,7 +165,7 @@ export default function Dropdown({
); );
const renderTriggerButton = () => ( const renderTriggerButton = () => (
<ShadTooltip content={!value ? (tooltip as string) : ""}> <div className="flex w-full flex-col">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
disabled={ disabled={
@ -199,7 +203,9 @@ export default function Dropdown({
className="h-4 w-4" className="h-4 w-4"
/> />
)} )}
{value && filteredOptions.includes(value) ? value : placeholderName}{" "} {value && filteredOptions.includes(value)
? value
: placeholderName}{" "}
</span> </span>
<ForwardedIconComponent <ForwardedIconComponent
name="ChevronsUpDown" name="ChevronsUpDown"
@ -212,7 +218,12 @@ export default function Dropdown({
/> />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</ShadTooltip> {helperText && (
<span className="pt-2 text-xs text-muted-foreground">
{convertStringToHTML(helperText)}
</span>
)}
</div>
); );
const renderSearchInput = () => ( const renderSearchInput = () => (
@ -302,7 +313,7 @@ export default function Dropdown({
data-testid={`${option}-${index}-option`} data-testid={`${option}-${index}-option`}
> >
<div className="flex w-full items-center gap-2"> <div className="flex w-full items-center gap-2">
{optionsMetaData && optionsMetaData.length > 0 && ( {optionsMetaData?.[index]?.icon && (
<ForwardedIconComponent <ForwardedIconComponent
name={optionsMetaData?.[index]?.icon || "Unknown"} name={optionsMetaData?.[index]?.icon || "Unknown"}
className="h-4 w-4 shrink-0 text-primary" className="h-4 w-4 shrink-0 text-primary"

View file

@ -19,6 +19,7 @@ export default function IntComponent({
disabled, disabled,
editNode = false, editNode = false,
id = "", id = "",
readonly,
}: InputProps<number, IntComponentType>): JSX.Element { }: InputProps<number, IntComponentType>): JSX.Element {
const min = -Infinity; const min = -Infinity;
// Clear component state // Clear component state
@ -59,6 +60,9 @@ export default function IntComponent({
); );
}; };
const DISABLED_INPUT_CLASS =
"cursor-default bg-secondary border-border border rounded-md py-2 px-3 text-sm text-input placeholder:text-input";
const handleNumberChange = (newValue) => { const handleNumberChange = (newValue) => {
handleOnNewValue({ value: Number(newValue) }); handleOnNewValue({ value: Number(newValue) });
}; };
@ -87,26 +91,35 @@ export default function IntComponent({
min={getMinValue()} min={getMinValue()}
max={getMaxValue()} max={getMaxValue()}
onChange={handleNumberChange} onChange={handleNumberChange}
isDisabled={disabled || readonly}
value={value ?? ""} value={value ?? ""}
> >
<NumberInputField <NumberInputField
className={getInputClassName()} className={
disabled || readonly ? DISABLED_INPUT_CLASS : getInputClassName()
}
onChange={handleChangeInput} onChange={handleChangeInput}
onKeyDown={(event) => handleKeyDown(event, value, "")} onKeyDown={(event) => handleKeyDown(event, value, "")}
onInput={handleInputChange} onInput={handleInputChange}
disabled={disabled} disabled={disabled || readonly}
placeholder={editNode ? "Integer number" : "Type an integer number"} placeholder={editNode ? "Integer number" : "Type an integer number"}
data-testid={id} data-testid={id}
ref={inputRef} ref={inputRef}
/> />
<NumberInputStepper className={stepperClassName}> <NumberInputStepper className={stepperClassName}>
<NumberIncrementStepper className={incrementStepperClassName}> <NumberIncrementStepper
className={incrementStepperClassName}
_disabled={{ cursor: "default" }}
>
<PlusIcon <PlusIcon
className={iconClassName} className={iconClassName}
strokeWidth={ICON_STROKE_WIDTH} strokeWidth={ICON_STROKE_WIDTH}
/> />
</NumberIncrementStepper> </NumberIncrementStepper>
<NumberDecrementStepper className={decrementStepperClassName}> <NumberDecrementStepper
className={decrementStepperClassName}
_disabled={{ cursor: "default" }}
>
<MinusIcon <MinusIcon
className={iconClassName} className={iconClassName}
strokeWidth={ICON_STROKE_WIDTH} strokeWidth={ICON_STROKE_WIDTH}

View file

@ -65,6 +65,7 @@ export function ParameterRenderComponent({
disabled, disabled,
nodeClass, nodeClass,
handleNodeClass, handleNodeClass,
helperText: templateData?.helper_text,
readonly: templateData.readonly, readonly: templateData.readonly,
placeholder, placeholder,
isToolMode, isToolMode,

View file

@ -11,6 +11,7 @@ export type BaseInputProps<valueType = any> = {
handleOnNewValue: handleOnNewValueType; handleOnNewValue: handleOnNewValueType;
disabled: boolean; disabled: boolean;
nodeClass?: APIClassType; nodeClass?: APIClassType;
helperText?: string;
handleNodeClass?: (value: any, code?: string, type?: string) => void; handleNodeClass?: (value: any, code?: string, type?: string) => void;
readonly?: boolean; readonly?: boolean;
placeholder?: string; placeholder?: string;

View file

@ -53,10 +53,12 @@ export function getCustomParameterTitle({
title, title,
nodeId, nodeId,
isFlexView, isFlexView,
required,
}: { }: {
title: string; title: string;
nodeId: string; nodeId: string;
isFlexView: boolean; isFlexView: boolean;
required: boolean;
}) { }) {
return ( return (
<div className={cn(isFlexView && "max-w-56 truncate")}> <div className={cn(isFlexView && "max-w-56 truncate")}>
@ -66,6 +68,7 @@ export function getCustomParameterTitle({
> >
{title} {title}
</span> </span>
{required && <span className="text-red-500">*</span>}
</div> </div>
); );
} }

View file

@ -1,3 +1,5 @@
import DOMPurify from "dompurify";
import React from "react";
import { FieldParserType } from "../types/api"; import { FieldParserType } from "../types/api";
function toSnakeCase(str: string): string { function toSnakeCase(str: string): string {
@ -134,3 +136,13 @@ export const getStatusColor = (status: string): string => {
return ""; return "";
}; };
export const convertStringToHTML = (htmlString: string): JSX.Element => {
return React.createElement("span", {
dangerouslySetInnerHTML: { __html: sanitizeHTML(htmlString) },
});
};
export const sanitizeHTML = (htmlString: string): string => {
return DOMPurify.sanitize(htmlString);
};

View file

@ -28,14 +28,11 @@ test(
await page await page
.getByTestId("outputsChat Output") .getByTestId("outputsChat Output")
.dragTo(page.locator('//*[@id="react-flow-id"]')); .hover()
await page.mouse.up(); .then(async () => {
await page.mouse.down(); await page.getByTestId("add-component-button-chat-output").click();
});
await adjustScreenView(page);
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click(); await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click(); await page.getByTestId("zoom_out").click();
@ -47,12 +44,8 @@ test(
await page await page
.getByTestId("inputsChat Input") .getByTestId("inputsChat Input")
.dragTo(page.locator('//*[@id="react-flow-id"]')); .dragTo(page.locator('//*[@id="react-flow-id"]'), {
await page.mouse.up(); targetPosition: { x: 100, y: 100 },
await page.mouse.down();
await page.waitForSelector('[data-testid="fit_view"]', {
timeout: 100000,
}); });
await page.getByTestId("sidebar-search-input").click(); await page.getByTestId("sidebar-search-input").click();
@ -63,92 +56,21 @@ test(
await page await page
.getByTestId("outputsText Output") .getByTestId("outputsText Output")
.dragTo(page.locator('//*[@id="react-flow-id"]')); .dragTo(page.locator('//*[@id="react-flow-id"]'), {
await page.mouse.up(); targetPosition: { x: 300, y: 300 },
await page.mouse.down(); });
await adjustScreenView(page); await adjustScreenView(page);
await page.getByTestId("zoom_out").click(); await page
await page.getByTestId("zoom_out").click(); .getByTestId("handle-chatinput-noshownode-message-source")
await page.getByTestId("zoom_out").click(); .click();
await page.getByTestId("zoom_out").click();
const elementsChatInput = await page await page.getByTestId("handle-textoutput-shownode-text-left").click();
.locator('[data-testid="handle-chatinput-noshownode-message-source"]')
.all();
let visibleElementHandle; await page.getByTestId("handle-textoutput-shownode-message-right").click();
await page.getByTestId("handle-chatoutput-noshownode-text-target").click();
for (const element of elementsChatInput) {
if (await element.isVisible()) {
visibleElementHandle = element;
break;
}
}
// Click and hold on the first element
await visibleElementHandle.hover();
await page.mouse.down();
// Move to the second element
const elementsTextOutput = await page
.getByTestId("handle-textoutput-shownode-text-left")
.all();
for (const element of elementsTextOutput) {
if (await element.isVisible()) {
visibleElementHandle = element;
break;
}
}
await visibleElementHandle.hover();
// Release the mouse
await page.mouse.up();
await page.getByTestId("fit_view").click();
await page.getByTestId("fit_view").click();
await page.getByTestId("fit_view").click();
await page.getByTestId("fit_view").click();
//
const elementsTextOutputRight = await page
.locator('[data-testid="handle-textoutput-shownode-message-right"]')
.all();
for (const element of elementsTextOutputRight) {
if (await element.isVisible()) {
visibleElementHandle = element;
break;
}
}
// Click and hold on the first element
await visibleElementHandle.hover();
await page.mouse.down();
//
const elementsChatOutput = await page
.getByTestId("handle-chatoutput-noshownode-text-target")
.all();
for (const element of elementsChatOutput) {
if (await element.isVisible()) {
visibleElementHandle = element;
break;
}
}
await visibleElementHandle.hover();
// Release the mouse
await page.mouse.up();
await page.getByTestId("fit_view").click();
await page.getByText("Playground", { exact: true }).last().click(); await page.getByText("Playground", { exact: true }).last().click();
await page.waitForSelector('[data-testid="input-chat-playground"]', { await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000, timeout: 100000,

View file

@ -42,7 +42,7 @@ test(
await page.getByText("Edit Prompt", { exact: true }).click(); await page.getByText("Edit Prompt", { exact: true }).click();
await page.getByTestId("edit-prompt-sanitized").click(); await page.getByTestId("edit-prompt-sanitized").last().click();
await page await page
.getByTestId("modal-promptarea_prompt_template") .getByTestId("modal-promptarea_prompt_template")
@ -52,18 +52,19 @@ test(
let promptSanitizedText = await page let promptSanitizedText = await page
.getByTestId("edit-prompt-sanitized") .getByTestId("edit-prompt-sanitized")
.last()
.textContent(); .textContent();
expect(promptSanitizedText).toBe("THIS IS A TEST"); expect(promptSanitizedText).toBe("THIS IS A TEST");
await page.getByTestId("edit-prompt-sanitized").click(); await page.getByTestId("edit-prompt-sanitized").last().click();
await page.keyboard.press(`ControlOrMeta+a`); await page.keyboard.press(`ControlOrMeta+a`);
await page.keyboard.press("Backspace"); await page.keyboard.press("Backspace");
await page.getByText("Edit Prompt", { exact: true }).click(); await page.getByText("Edit Prompt", { exact: true }).click();
await page.getByTestId("edit-prompt-sanitized").click(); await page.getByTestId("edit-prompt-sanitized").last().click();
await page await page
.getByTestId("modal-promptarea_prompt_template") .getByTestId("modal-promptarea_prompt_template")
@ -73,6 +74,7 @@ test(
promptSanitizedText = await page promptSanitizedText = await page
.getByTestId("edit-prompt-sanitized") .getByTestId("edit-prompt-sanitized")
.last()
.textContent(); .textContent();
expect(promptSanitizedText).toBe("THIS IS A TEST 2"); expect(promptSanitizedText).toBe("THIS IS A TEST 2");

View file

@ -20,9 +20,10 @@ test("chat_io_teste", { tag: ["@release", "@workspace"] }, async ({ page }) => {
await page await page
.getByTestId("outputsChat Output") .getByTestId("outputsChat Output")
.dragTo(page.locator('//*[@id="react-flow-id"]')); .hover()
await page.mouse.up(); .then(async () => {
await page.mouse.down(); await page.getByTestId("add-component-button-chat-output").click();
});
await page.getByTestId("sidebar-search-input").click(); await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input"); await page.getByTestId("sidebar-search-input").fill("chat input");
@ -32,59 +33,19 @@ test("chat_io_teste", { tag: ["@release", "@workspace"] }, async ({ page }) => {
await page await page
.getByTestId("inputsChat Input") .getByTestId("inputsChat Input")
.dragTo(page.locator('//*[@id="react-flow-id"]')); .dragTo(page.locator('//*[@id="react-flow-id"]'), {
await page.mouse.up(); targetPosition: { x: 100, y: 100 },
await page.mouse.down(); });
await page.waitForSelector('[data-testid="fit_view"]', { await page.waitForSelector('[data-testid="fit_view"]', {
timeout: 100000, timeout: 100000,
}); });
await page.getByTestId("fit_view").click(); await page.getByTestId("fit_view").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
const elementsChatInput = await page await page.getByTestId("handle-chatinput-noshownode-message-source").click();
.locator('[data-testid="handle-chatinput-noshownode-message-source"]') await page.getByTestId("handle-chatoutput-noshownode-text-target").click();
.all();
let visibleElementHandle;
for (const element of elementsChatInput) {
if (await element.isVisible()) {
visibleElementHandle = element;
break;
}
}
// Click and hold on the first element
await visibleElementHandle.hover();
await page.mouse.down();
// Move to the second element
const elementsChatOutput = await page
.getByTestId("handle-chatoutput-noshownode-text-target")
.all();
for (const element of elementsChatOutput) {
if (await element.isVisible()) {
visibleElementHandle = element;
break;
}
}
await visibleElementHandle.hover();
// Release the mouse
await page.mouse.up();
await page.getByTestId("fit_view").click();
await page.getByText("Playground", { exact: true }).last().click(); await page.getByText("Playground", { exact: true }).last().click();
await page.waitForSelector('[data-testid="input-chat-playground"]', { await page.waitForSelector('[data-testid="input-chat-playground"]', {
timeout: 100000, timeout: 100000,

View file

@ -95,17 +95,13 @@ test(
.getByTestId("inputlist_str_urls_0") .getByTestId("inputlist_str_urls_0")
.fill("https://www.example.com"); .fill("https://www.example.com");
// Connect text output to first chat output await page.getByTestId("handle-url-shownode-text-right").nth(0).click();
const urlTextOutput = await page await page.waitForTimeout(600);
.getByTestId("handle-url-shownode-text-right")
.nth(0); await page
await urlTextOutput.hover();
await page.mouse.down();
const firstChatInput = await page
.getByTestId("handle-chatoutput-noshownode-text-target") .getByTestId("handle-chatoutput-noshownode-text-target")
.nth(0); .nth(0)
await firstChatInput.hover(); .click();
await page.mouse.up();
// Run flow and test text output inspection // Run flow and test text output inspection
await page.getByTestId("button_run_url").first().click(); await page.getByTestId("button_run_url").first().click();
@ -120,16 +116,16 @@ test(
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
// Connect dataframe output to second chat output // Connect dataframe output to second chat output
const urlDataframeOutput = await page await page
.getByTestId("handle-url-shownode-dataframe-right") .getByTestId("handle-url-shownode-dataframe-right")
.nth(0); .nth(0)
await urlDataframeOutput.hover(); .click();
await page.mouse.down(); await page.waitForTimeout(600);
const secondChatInput = await page await page
.getByTestId("handle-chatoutput-noshownode-text-target") .getByTestId("handle-chatoutput-noshownode-text-target")
.nth(1); .nth(1)
await secondChatInput.hover(); .click();
await page.mouse.up(); await page.waitForTimeout(600);
// Run and verify text output is still shown // Run and verify text output is still shown
await page.getByTestId("button_run_url").first().click(); await page.getByTestId("button_run_url").first().click();
@ -143,6 +139,7 @@ test(
}, },
); );
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
await page.waitForTimeout(600);
// Remove text connection // Remove text connection
const textEdge = await page.locator(".react-flow__edge").first(); const textEdge = await page.locator(".react-flow__edge").first();
@ -162,7 +159,7 @@ test(
}, },
); );
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
await page.waitForTimeout(600);
// Remove all connections // Remove all connections
const dataEdge = await page.locator(".react-flow__edge").first(); const dataEdge = await page.locator(".react-flow__edge").first();
await dataEdge.click(); await dataEdge.click();