feat: redesign sidebar with new components and features (#4307)

* Added required ShadCN Components

* Added required colors

* Added required icons

* Changed backend to not return beta tag

* Added sidebar categories api call

* Added required use-mobile hook

* Refactored icons to allow sizing

* Added type for sidebar category

* Allowed no name on shortcut display

* Added flow sidebar component that uses ShadCN sidebar

* Added SidebarDraggableComponent with the new style

* Replaced sidebar with current one on FlowPage

* Added sidebar fixed footer with options to go to store and add custom component

* Updated sidebar categories

* Updated background color for sidebar

* Changed size of Components title

* Added color to PageComponent

* Added required icons

* added CustomComponent category and removed custom component from helpers

* create hook useAddComponent

* Use hook to add components both in PageComponent and flowSidebarComponent

* Implement search

* Implement searching and filtering by clicking on edge

* Added check to see if store is present

* Updated colors to match new color schema

* Changed styling of filter component

* Added Beta and Legacy badges

* Implement Show Beta Components and Show Legacy Components

* Fixed styling for sidebar config when collapsed

* Refactored search to filter for tags

* Refactor useeffect

* Updated config button styling

* Implemented keyboard navigation

* Fixed filtering

* Updated color of canvas

* Implemented disclosure on sidebar settings and fetched bundles

* Added temp sidebar bundles

* Fixed badge styling

* Added bundles to categories response in frontend

* Added legacy to components

* Added link to store instead of langflow.store

* Added required data-testids

* Fixed tests to use new data-testids and new sidebar disposition

* Fix github star bug

* Fixed tests that used the custom component

* Changed test to test beta and legacy checkers

* added a test for keyboard navigation on sidebar

* Added a test to check component add by hover the plus button

* [autofix.ci] apply automated fixes

* updated sidebar switch change

* Removed changes on Backend and used only Frontend constants for categories

* merge fix

* [autofix.ci] apply automated fixes

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

* 📝 (custom_component): Add CustomComponent class with input and output definitions
📝 (custom_component): Create CustomComponent class with display name, description, and documentation link
📝 (custom_component): Define input and output properties for CustomComponent class
📝 (custom_component): Implement build_output method in CustomComponent class
📝 (Ollama): Remove unnecessary whitespace in SvgOllama component

* formatting

* ♻️ (custom_component/__init__.py): refactor import statement to match the correct case of the file name for better consistency and readability

* 🔧 (FlowPage/index.tsx): remove FlowToolbar component when view prop is false to improve UI consistency

* 📝 (integration-side-bar.spec.ts): Comment out unnecessary code blocks related to API requests and modals to improve test readability and focus on the main test scenarios.

* [autofix.ci] apply automated fixes

*  (flowSidebarComponent/index.tsx): add data-testid attribute to Sidebar component for testing purposes
🔧 (integration-side-bar.spec.ts): update test to use new data-testid attribute 'shad-sidebar' for Sidebar component to match changes in the codebase

*  (stop-building.spec.ts): refactor test case to use a more descriptive test element for clicking on the sidebar custom component button

* format

* 🐛 (AstraDB): Fix variable naming inconsistency for isDark to isdark in AstraDB component
🐛 (HCD): Fix variable naming inconsistency for isDark to isdark in HCD component
🐛 (index.tsx): Fix variable naming inconsistency for isDark to isdark in index.tsx files
📝 (flowSidebarComponent): Refactor search functionality to normalize search terms and improve metadata search
📝 (filterEdge-shard-1.spec.ts): Update test to check visibility of specific model specs in the sidebar

* [autofix.ci] apply automated fixes

*  (decisionFlow.spec.ts): add a delay of 500ms to ensure proper timing in the test case execution

*  (decisionFlow.spec.ts): Update the X positions of elements in the flow to improve the visual representation and alignment of the elements.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Lucas Oliveira 2024-10-31 19:51:36 -03:00 committed by GitHub
commit 794848d5e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
122 changed files with 2365 additions and 802 deletions

View file

@ -6,7 +6,16 @@ from typing import TYPE_CHECKING, Annotated
from uuid import UUID
import sqlalchemy as sa
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Request, UploadFile, status
from fastapi import (
APIRouter,
BackgroundTasks,
Body,
Depends,
HTTPException,
Request,
UploadFile,
status,
)
from loguru import logger
from sqlmodel import select
@ -17,7 +26,6 @@ from langflow.api.v1.schemas import (
CustomComponentResponse,
InputValueRequest,
RunResponse,
SidebarCategoriesResponse,
SimplifiedAPIRequest,
TaskStatusResponse,
UpdateCustomComponentRequest,
@ -37,7 +45,9 @@ from langflow.services.auth.utils import api_key_security, get_current_active_us
from langflow.services.cache.utils import save_uploaded_file
from langflow.services.database.models.flow import Flow
from langflow.services.database.models.flow.model import FlowRead
from langflow.services.database.models.flow.utils import get_all_webhook_components_in_flow
from langflow.services.database.models.flow.utils import (
get_all_webhook_components_in_flow,
)
from langflow.services.database.models.user.model import User, UserRead
from langflow.services.deps import (
get_session_service,
@ -47,7 +57,6 @@ from langflow.services.deps import (
)
from langflow.services.settings.feature_flags import FEATURE_FLAGS
from langflow.services.telemetry.schema import RunPayload
from langflow.utils.constants import SIDEBAR_CATEGORIES
from langflow.utils.version import get_version_info
if TYPE_CHECKING:
@ -110,7 +119,11 @@ async def simple_run_flow(
graph_data = process_tweaks(graph_data, input_request.tweaks or {}, stream=stream)
graph = Graph.from_payload(graph_data, flow_id=flow_id_str, user_id=str(user_id), flow_name=flow.name)
inputs = [
InputValueRequest(components=[], input_value=input_request.input_value, type=input_request.input_type)
InputValueRequest(
components=[],
input_value=input_request.input_value,
type=input_request.input_type,
)
]
if input_request.output_component:
outputs = [input_request.output_component]
@ -248,7 +261,10 @@ async def simplified_run_flow(
background_tasks.add_task(
telemetry_service.log_package_run,
RunPayload(
run_is_webhook=False, run_seconds=int(end_time - start_time), run_success=True, run_error_message=""
run_is_webhook=False,
run_seconds=int(end_time - start_time),
run_success=True,
run_error_message="",
),
)
@ -360,7 +376,11 @@ async def webhook_run_flow(
return {"message": "Task started in the background", "status": "in progress"}
@router.post("/run/advanced/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
@router.post(
"/run/advanced/{flow_id}",
response_model=RunResponse,
response_model_exclude_none=True,
)
async def experimental_run_flow(
*,
session: DbSession,
@ -631,11 +651,9 @@ async def get_config():
settings_service: SettingsService = get_settings_service()
return {"feature_flags": FEATURE_FLAGS, **settings_service.settings.model_dump()}
return {
"feature_flags": FEATURE_FLAGS,
**settings_service.settings.model_dump(),
}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.get("/sidebar_categories")
async def get_sidebar_categories() -> SidebarCategoriesResponse:
return SidebarCategoriesResponse(categories=SIDEBAR_CATEGORIES)

View file

@ -4,7 +4,14 @@ from pathlib import Path
from typing import Any
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_serializer,
field_validator,
model_serializer,
)
from langflow.graph.schema import RunOutputs
from langflow.graph.utils import serialize_field
@ -316,7 +323,11 @@ class InputValueRequest(BaseModel):
},
{"components": ["Component Name"], "input_value": "input_value"},
{"input_value": "input_value"},
{"components": ["Component Name"], "input_value": "input_value", "session": "session_id"},
{
"components": ["Component Name"],
"input_value": "input_value",
"session": "session_id",
},
{"input_value": "input_value", "session": "session_id"},
{"type": "chat", "input_value": "input_value"},
{"type": "json", "input_value": '{"key": "value"}'},
@ -357,14 +368,3 @@ class ConfigResponse(BaseModel):
auto_saving_interval: int
health_check_max_retries: int
max_file_size_upload: int
class SidebarCategory(BaseModel):
display_name: str
name: str
icon: str
beta: bool = False
class SidebarCategoriesResponse(BaseModel):
categories: list[SidebarCategory]

View file

@ -10,7 +10,15 @@ import orjson
STREAM_INFO_TEXT = "Stream the response from the model. Streaming works only in Chat."
NODE_FORMAT_ATTRIBUTES = ["beta", "icon", "display_name", "output_types", "edited", "metadata"]
NODE_FORMAT_ATTRIBUTES = [
"beta",
"legacy",
"icon",
"display_name",
"output_types",
"edited",
"metadata",
]
FIELD_FORMAT_ATTRIBUTES = [

View file

@ -1,6 +1,7 @@
from . import (
agents,
chains,
custom_component,
documentloaders,
embeddings,
helpers,
@ -26,6 +27,7 @@ __all__ = [
"helpers",
"inputs",
"link_extractors",
"custom_component",
"memories",
"models",
"output_parsers",

View file

@ -14,11 +14,22 @@ class JsonAgentComponent(LCAgentComponent):
display_name = "JsonAgent"
description = "Construct a json agent from an LLM and tools."
name = "JsonAgent"
legacy: bool = True
inputs = [
*LCAgentComponent._base_inputs,
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
FileInput(name="path", display_name="File Path", file_types=["json", "yaml", "yml"], required=True),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
FileInput(
name="path",
display_name="File Path",
file_types=["json", "yaml", "yml"],
required=True,
),
]
def build_agent(self) -> AgentExecutor:

View file

@ -9,11 +9,22 @@ class VectorStoreAgentComponent(LCAgentComponent):
display_name = "VectorStoreAgent"
description = "Construct an agent from a Vector Store."
name = "VectorStoreAgent"
legacy: bool = True
inputs = [
*LCAgentComponent._base_inputs,
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
HandleInput(name="vectorstore", display_name="Vector Store", input_types=["VectorStoreInfo"], required=True),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(
name="vectorstore",
display_name="Vector Store",
input_types=["VectorStoreInfo"],
required=True,
),
]
def build_agent(self) -> AgentExecutor:

View file

@ -9,10 +9,16 @@ class VectorStoreRouterAgentComponent(LCAgentComponent):
display_name = "VectorStoreRouterAgent"
description = "Construct an agent from a Vector Store Router."
name = "VectorStoreRouterAgent"
legacy: bool = True
inputs = [
*LCAgentComponent._base_inputs,
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(
name="vectorstores",
display_name="Vector Stores",

View file

@ -9,12 +9,21 @@ class ConversationChainComponent(LCChainComponent):
display_name = "ConversationChain"
description = "Chain to have a conversation and load context from memory."
name = "ConversationChain"
legacy: bool = True
inputs = [
MultilineInput(
name="input_value", display_name="Input", info="The input value to pass to the chain.", required=True
name="input_value",
display_name="Input",
info="The input value to pass to the chain.",
required=True,
),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
HandleInput(
name="memory",
display_name="Memory",
@ -28,7 +37,10 @@ class ConversationChainComponent(LCChainComponent):
else:
chain = ConversationChain(llm=self.llm, memory=self.memory)
result = chain.invoke({"input": self.input_value}, config={"callbacks": self.get_langchain_callbacks()})
result = chain.invoke(
{"input": self.input_value},
config={"callbacks": self.get_langchain_callbacks()},
)
if isinstance(result, dict):
result = result.get(chain.output_key, "")

View file

@ -10,18 +10,28 @@ class LLMCheckerChainComponent(LCChainComponent):
description = "Chain for question-answering with self-verification."
documentation = "https://python.langchain.com/docs/modules/chains/additional/llm_checker"
name = "LLMCheckerChain"
legacy: bool = True
inputs = [
MultilineInput(
name="input_value", display_name="Input", info="The input value to pass to the chain.", required=True
name="input_value",
display_name="Input",
info="The input value to pass to the chain.",
required=True,
),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
]
def invoke_chain(self) -> Message:
chain = LLMCheckerChain.from_llm(llm=self.llm)
response = chain.invoke(
{chain.input_key: self.input_value}, config={"callbacks": self.get_langchain_callbacks()}
{chain.input_key: self.input_value},
config={"callbacks": self.get_langchain_callbacks()},
)
result = response.get(chain.output_key, "")
result = str(result)

View file

@ -11,12 +11,21 @@ class LLMMathChainComponent(LCChainComponent):
description = "Chain that interprets a prompt and executes python code to do math."
documentation = "https://python.langchain.com/docs/modules/chains/additional/llm_math"
name = "LLMMathChain"
legacy: bool = True
inputs = [
MultilineInput(
name="input_value", display_name="Input", info="The input value to pass to the chain.", required=True
name="input_value",
display_name="Input",
info="The input value to pass to the chain.",
required=True,
),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
]
outputs = [Output(display_name="Text", name="text", method="invoke_chain")]
@ -24,7 +33,8 @@ class LLMMathChainComponent(LCChainComponent):
def invoke_chain(self) -> Message:
chain = LLMMathChain.from_llm(llm=self.llm)
response = chain.invoke(
{chain.input_key: self.input_value}, config={"callbacks": self.get_langchain_callbacks()}
{chain.input_key: self.input_value},
config={"callbacks": self.get_langchain_callbacks()},
)
result = response.get(chain.output_key, "")
result = str(result)

View file

@ -9,10 +9,14 @@ class RetrievalQAComponent(LCChainComponent):
display_name = "Retrieval QA"
description = "Chain for question-answering querying sources from a retriever."
name = "RetrievalQA"
legacy: bool = True
inputs = [
MultilineInput(
name="input_value", display_name="Input", info="The input value to pass to the chain.", required=True
name="input_value",
display_name="Input",
info="The input value to pass to the chain.",
required=True,
),
DropdownInput(
name="chain_type",
@ -22,8 +26,18 @@ class RetrievalQAComponent(LCChainComponent):
value="Stuff",
advanced=True,
),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
HandleInput(name="retriever", display_name="Retriever", input_types=["Retriever"], required=True),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(
name="retriever",
display_name="Retriever",
input_types=["Retriever"],
required=True,
),
HandleInput(
name="memory",
display_name="Memory",
@ -52,7 +66,10 @@ class RetrievalQAComponent(LCChainComponent):
return_source_documents=True,
)
result = runnable.invoke({"query": self.input_value}, config={"callbacks": self.get_langchain_callbacks()})
result = runnable.invoke(
{"query": self.input_value},
config={"callbacks": self.get_langchain_callbacks()},
)
source_docs = self.to_data(result.get("source_documents", keys=[]))
result_str = str(result.get("result", ""))

View file

@ -16,17 +16,38 @@ class SQLGeneratorComponent(LCChainComponent):
display_name = "Natural Language to SQL"
description = "Generate SQL from natural language."
name = "SQLGenerator"
legacy: bool = True
inputs = [
MultilineInput(
name="input_value", display_name="Input", info="The input value to pass to the chain.", required=True
name="input_value",
display_name="Input",
info="The input value to pass to the chain.",
required=True,
),
HandleInput(
name="llm",
display_name="Language Model",
input_types=["LanguageModel"],
required=True,
),
HandleInput(
name="db",
display_name="SQLDatabase",
input_types=["SQLDatabase"],
required=True,
),
HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True),
HandleInput(name="db", display_name="SQLDatabase", input_types=["SQLDatabase"], required=True),
IntInput(
name="top_k", display_name="Top K", info="The number of results per select statement to return.", value=5
name="top_k",
display_name="Top K",
info="The number of results per select statement to return.",
value=5,
),
MultilineInput(
name="prompt",
display_name="Prompt",
info="The prompt must contain `{question}`.",
),
MultilineInput(name="prompt", display_name="Prompt", info="The prompt must contain `{question}`."),
]
outputs = [Output(display_name="Text", name="text", method="invoke_chain")]
@ -48,7 +69,8 @@ class SQLGeneratorComponent(LCChainComponent):
sql_query_chain = create_sql_query_chain(llm=self.llm, db=self.db, prompt=prompt_template, k=self.top_k)
query_writer: Runnable = sql_query_chain | {"query": lambda x: x.replace("SQLQuery:", "").strip()}
response = query_writer.invoke(
{"question": self.input_value}, config={"callbacks": self.get_langchain_callbacks()}
{"question": self.input_value},
config={"callbacks": self.get_langchain_callbacks()},
)
query = response.get("query")
self.status = query

View file

@ -0,0 +1,5 @@
from .custom_component import CustomComponent
__all__ = [
"CustomComponent",
]

View file

@ -2,7 +2,6 @@ from .combine_text import CombineTextComponent
from .create_list import CreateListComponent
from .csv_to_data import CSVToDataComponent
from .current_date import CurrentDateComponent
from .custom_component import CustomComponent
from .data_conditional_router import DataConditionalRouterComponent
from .extract_key import ExtractDataKeyComponent
from .filter_data import FilterDataComponent
@ -25,7 +24,6 @@ __all__ = [
"CombineTextComponent",
"CreateListComponent",
"CurrentDateComponent",
"CustomComponent",
"DataConditionalRouterComponent",
"DataFilterComponent",
"ExtractDataKeyComponent",

View file

@ -21,6 +21,7 @@ class JSONDocumentBuilder(CustomComponent):
display_name: str = "JSON Document Builder"
description: str = "Build a Document containing a JSON object using a key and another Document page content."
name = "JSONDocumentBuilder"
legacy: bool = True
output_types: list[str] = ["Document"]
documentation: str = "https://docs.langflow.org/components/utilities#json-document-builder"

View file

@ -11,6 +11,7 @@ class AmazonKendraRetrieverComponent(CustomComponent):
description: str = "Retriever that uses the Amazon Kendra API."
name = "AmazonKendra"
icon = "Amazon"
legacy: bool = True
def build_config(self):
return {

View file

@ -3,7 +3,10 @@ from typing import cast
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
from langflow.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
from langflow.base.vectorstores.model import (
LCVectorStoreComponent,
check_cached_vector_store,
)
from langflow.field_typing import Retriever, VectorStore
from langflow.io import (
DropdownInput,
@ -22,6 +25,7 @@ class CohereRerankComponent(LCVectorStoreComponent):
description = "Rerank documents using the Cohere API and a retriever."
name = "CohereRerank"
icon = "Cohere"
legacy: bool = True
inputs = [
MultilineInput(

View file

@ -11,6 +11,7 @@ class MetalRetrieverComponent(CustomComponent):
display_name: str = "Metal Retriever"
description: str = "Retriever that uses the Metal API."
name = "MetalRetriever"
legacy: bool = True
def build_config(self):
return {

View file

@ -9,6 +9,7 @@ class MultiQueryRetrieverComponent(CustomComponent):
description = "Initialize from llm using default template."
documentation = "https://python.langchain.com/docs/modules/data_connection/retrievers/how_to/MultiQueryRetriever"
name = "MultiQueryRetriever"
legacy: bool = True
def build_config(self):
return {

View file

@ -2,9 +2,18 @@ from typing import Any, cast
from langchain.retrievers import ContextualCompressionRetriever
from langflow.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
from langflow.base.vectorstores.model import (
LCVectorStoreComponent,
check_cached_vector_store,
)
from langflow.field_typing import Retriever, VectorStore
from langflow.io import DropdownInput, HandleInput, MultilineInput, SecretStrInput, StrInput
from langflow.io import (
DropdownInput,
HandleInput,
MultilineInput,
SecretStrInput,
StrInput,
)
from langflow.schema import Data
from langflow.schema.dotdict import dotdict
from langflow.template.field.base import Output
@ -14,6 +23,7 @@ class NvidiaRerankComponent(LCVectorStoreComponent):
display_name = "NVIDIA Rerank"
description = "Rerank documents using the NVIDIA API and a retriever."
icon = "NVIDIA"
legacy: bool = True
inputs = [
MultilineInput(
@ -28,7 +38,10 @@ class NvidiaRerankComponent(LCVectorStoreComponent):
info="The base URL of the NVIDIA API. Defaults to https://integrate.api.nvidia.com/v1.",
),
DropdownInput(
name="model", display_name="Model", options=["nv-rerank-qa-mistral-4b:1"], value="nv-rerank-qa-mistral-4b:1"
name="model",
display_name="Model",
options=["nv-rerank-qa-mistral-4b:1"],
value="nv-rerank-qa-mistral-4b:1",
),
SecretStrInput(name="api_key", display_name="API Key"),
HandleInput(name="retriever", display_name="Retriever", input_types=["Retriever"]),

View file

@ -13,6 +13,7 @@ class SelfQueryRetrieverComponent(Component):
description = "Retriever that uses a vector store and an LLM to generate the vector store queries."
name = "SelfQueryRetriever"
icon = "LangChain"
legacy: bool = True
inputs = [
HandleInput(
@ -48,7 +49,11 @@ class SelfQueryRetrieverComponent(Component):
]
outputs = [
Output(display_name="Retrieved Documents", name="documents", method="retrieve_documents"),
Output(
display_name="Retrieved Documents",
name="documents",
method="retrieve_documents",
),
]
def retrieve_documents(self) -> list[Data]:

View file

@ -8,6 +8,7 @@ class VectoStoreRetrieverComponent(CustomComponent):
display_name = "VectorStore Retriever"
description = "A vector store retriever"
name = "VectorStoreRetriever"
legacy: bool = True
def build_config(self):
return {

View file

@ -9,6 +9,7 @@ class VectorStoreInfoComponent(Component):
display_name = "VectorStoreInfo"
description = "Information about a VectorStore"
name = "VectorStoreInfo"
legacy: bool = True
inputs = [
MessageTextInput(
@ -41,5 +42,7 @@ class VectorStoreInfoComponent(Component):
"description": self.vectorstore_description,
}
return VectorStoreInfo(
vectorstore=self.input_vectorstore, description=self.vectorstore_description, name=self.vectorstore_name
vectorstore=self.input_vectorstore,
description=self.vectorstore_description,
name=self.vectorstore_name,
)

View file

@ -10,7 +10,14 @@ from pydantic.v1.fields import Undefined
from typing_extensions import override
from langflow.base.langchain_utilities.model import LCToolComponent
from langflow.inputs.inputs import BoolInput, DropdownInput, FieldTypes, HandleInput, MessageTextInput, MultilineInput
from langflow.inputs.inputs import (
BoolInput,
DropdownInput,
FieldTypes,
HandleInput,
MessageTextInput,
MultilineInput,
)
from langflow.io import Output
from langflow.schema import Data
from langflow.schema.dotdict import dotdict
@ -36,6 +43,7 @@ class PythonCodeStructuredTool(LCToolComponent):
name = "PythonCodeStructuredTool"
icon = "🐍"
field_order = ["name", "description", "tool_code", "return_direct", "tool_function"]
legacy: bool = True
inputs = [
MultilineInput(
@ -47,7 +55,12 @@ class PythonCodeStructuredTool(LCToolComponent):
real_time_refresh=True,
refresh_button=True,
),
MessageTextInput(name="tool_name", display_name="Tool Name", info="Enter the name of the tool.", required=True),
MessageTextInput(
name="tool_name",
display_name="Tool Name",
info="Enter the name of the tool.",
required=True,
),
MessageTextInput(
name="tool_description",
display_name="Description",
@ -192,7 +205,10 @@ class PythonCodeStructuredTool(LCToolComponent):
schema_annotation = Any
schema_fields[field_name] = (
schema_annotation,
Field(default=func_arg.get("default", Undefined), description=field_description),
Field(
default=func_arg.get("default", Undefined),
description=field_description,
),
)
if "temp_annotation_type" in _globals:
@ -214,7 +230,9 @@ class PythonCodeStructuredTool(LCToolComponent):
"""This function is called after the code validation is done."""
frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node)
frontend_node["template"] = self.update_build_config(
frontend_node["template"], frontend_node["template"]["tool_code"]["value"], "tool_code"
frontend_node["template"],
frontend_node["template"]["tool_code"]["value"],
"tool_code",
)
frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node)
for key in frontend_node["template"]:

View file

@ -19,6 +19,7 @@ class SearXNGToolComponent(LCToolComponent):
display_name = "SearXNG Search Tool"
description = "A component that searches for tools using SearXNG."
name = "SearXNGTool"
legacy: bool = True
inputs = [
MessageTextInput(
@ -127,7 +128,10 @@ class SearXNGToolComponent(LCToolComponent):
schema_fields = {
"query": (str, Field(..., description="The query to search for.")),
"categories": (list[str], Field(default=[], description="The categories to search in.")),
"categories": (
list[str],
Field(default=[], description="The categories to search in."),
),
}
searx_search_schema = create_model("SearxSearchSchema", **schema_fields)

View file

@ -61,6 +61,7 @@ ATTR_FUNC_MAPPING: dict[str, Callable] = {
"display_name": getattr_return_str,
"description": getattr_return_str,
"beta": getattr_return_bool,
"legacy": getattr_return_bool,
"documentation": getattr_return_str,
"icon": validate_icon,
"frozen": getattr_return_bool,

View file

@ -158,6 +158,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"lf_version": "1.0.16",
"metadata": {},
"output_types": [],
@ -380,6 +381,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"lf_version": "1.0.16",
"metadata": {},
"output_types": [],
@ -589,6 +591,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"lf_version": "1.0.16",
"metadata": {},
"output_types": [],
@ -1203,6 +1206,7 @@
],
"frozen": false,
"icon": "calculator",
"legacy": false,
"lf_version": "1.0.16",
"metadata": {},
"official": false,
@ -1322,6 +1326,7 @@
"code"
],
"frozen": false,
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -108,6 +108,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -324,6 +325,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -445,6 +447,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -647,6 +650,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -156,6 +156,7 @@
],
"frozen": false,
"icon": "layout-template",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -288,6 +289,7 @@
],
"frozen": false,
"icon": "braces",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -425,6 +427,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -667,6 +670,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -869,6 +873,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -629,6 +629,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -876,6 +877,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1202,6 +1204,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1388,6 +1391,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1528,6 +1532,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1783,6 +1788,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2043,6 +2049,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2373,6 +2380,7 @@
"is_composition": null,
"is_input": null,
"is_output": null,
"legacy": false,
"metadata": {},
"name": "",
"output_types": [],
@ -2488,6 +2496,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2814,6 +2823,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -3264,6 +3274,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -3738,6 +3749,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -4070,6 +4082,7 @@
"is_composition": null,
"is_input": null,
"is_output": null,
"legacy": false,
"metadata": {},
"name": "",
"output_types": [],
@ -4237,6 +4250,7 @@
"search_params"
],
"frozen": false,
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -161,6 +161,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -306,6 +307,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -523,6 +525,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -725,6 +728,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1048,6 +1052,7 @@
],
"frozen": false,
"icon": "braces",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1181,6 +1186,7 @@
],
"frozen": false,
"icon": "file-text",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -326,6 +326,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -576,6 +577,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -906,6 +908,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1097,6 +1100,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1242,6 +1246,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1501,6 +1506,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1763,6 +1769,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2097,6 +2104,7 @@
"is_composition": null,
"is_input": null,
"is_output": null,
"legacy": false,
"metadata": {},
"name": "",
"output_types": [],
@ -2216,6 +2224,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2436,6 +2445,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2687,6 +2697,7 @@
"search_params"
],
"frozen": false,
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -135,6 +135,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -280,6 +281,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -504,6 +506,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -830,6 +833,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1023,6 +1027,7 @@
],
"frozen": false,
"icon": "message-square-more",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -389,6 +389,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -588,6 +589,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -915,6 +917,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -1199,6 +1202,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -1320,6 +1324,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -1441,6 +1446,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -1569,6 +1575,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -1920,6 +1927,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -2271,6 +2279,7 @@
],
"frozen": false,
"icon": "CrewAI",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],

View file

@ -398,6 +398,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -620,6 +621,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"output_types": [],
@ -829,6 +831,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1442,6 +1445,7 @@
"search_params"
],
"frozen": false,
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"official": false,
@ -2319,6 +2323,7 @@
],
"frozen": false,
"icon": "calculator",
"legacy": false,
"lf_version": "1.0.15",
"metadata": {},
"official": false,

View file

@ -291,6 +291,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -524,6 +525,7 @@
],
"frozen": false,
"icon": "AstraDB",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1020,6 +1022,7 @@
],
"frozen": false,
"icon": "braces",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1157,6 +1160,7 @@
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1302,6 +1306,7 @@
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1494,6 +1499,7 @@
],
"frozen": false,
"icon": "scissors-line-dashed",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1639,6 +1645,7 @@
],
"frozen": false,
"icon": "file-text",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -1788,6 +1795,7 @@
],
"frozen": false,
"icon": "AstraDB",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2302,6 +2310,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -2778,6 +2787,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [
@ -3246,6 +3256,7 @@
],
"frozen": false,
"icon": "OpenAI",
"legacy": false,
"metadata": {},
"output_types": [],
"outputs": [

View file

@ -49,6 +49,8 @@ class FrontendNode(BaseModel):
"""Order of the fields in the frontend node."""
beta: bool = False
"""Whether the frontend node is in beta."""
legacy: bool = False
"""Whether the frontend node is legacy."""
error: str | None = None
"""Error message for the frontend node."""
edited: bool = False
@ -81,7 +83,10 @@ class FrontendNode(BaseModel):
if "output_types" in result and not result.get("outputs"):
for base_class in result["output_types"]:
output = Output(
display_name=base_class, name=base_class.lower(), types=[base_class], selected=base_class
display_name=base_class,
name=base_class.lower(),
types=[base_class],
selected=base_class,
)
result["outputs"].append(output.model_dump())

View file

@ -47,6 +47,7 @@ class CustomComponentFrontendNode(FrontendNode):
name: str = "CustomComponent"
display_name: str | None = "CustomComponent"
beta: bool = False
legacy: bool = False
template: Template = Template(
type_name="CustomComponent",
fields=[
@ -72,6 +73,7 @@ class ComponentFrontendNode(FrontendNode):
name: str = "Component"
display_name: str | None = "Component"
beta: bool = False
legacy: bool = False
template: Template = Template(
type_name="Component",
fields=[

View file

@ -52,7 +52,18 @@ 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"]
DIRECT_TYPES = [
"str",
"bool",
"dict",
"int",
"float",
"Any",
"prompt",
"code",
"NestedDict",
"table",
]
LOADERS_INFO: list[dict[str, Any]] = [
@ -175,29 +186,3 @@ MESSAGE_SENDER_NAME_AI = "AI"
MESSAGE_SENDER_NAME_USER = "User"
MAX_TEXT_LENGTH = 99999
SIDEBAR_CATEGORIES = [
{"display_name": "Saved", "name": "saved_components", "icon": "GradientSave"},
{"display_name": "Inputs", "name": "inputs", "icon": "Download"},
{"display_name": "Outputs", "name": "outputs", "icon": "Upload"},
{"display_name": "Prompts", "name": "prompts", "icon": "TerminalSquare"},
{"display_name": "Data", "name": "data", "icon": "Database"},
{"display_name": "Models", "name": "models", "icon": "BrainCircuit"},
{"display_name": "Helpers", "name": "helpers", "icon": "Wand2"},
{"display_name": "Vector Stores", "name": "vectorstores", "icon": "Layers"},
{"display_name": "Embeddings", "name": "embeddings", "icon": "Binary"},
{"display_name": "Agents", "name": "agents", "icon": "Bot"},
{"display_name": "Astra Assistants", "name": "astra_assistants", "icon": "Sparkles"},
{"display_name": "Chains", "name": "chains", "icon": "Link"},
{"display_name": "Loaders", "name": "documentloaders", "icon": "Paperclip"},
{"display_name": "Utilities", "name": "langchain_utilities", "icon": "PocketKnife"},
{"display_name": "Link Extractors", "name": "link_extractors", "icon": "Link2"},
{"display_name": "Memories", "name": "memories", "icon": "Cpu"},
{"display_name": "Output Parsers", "name": "output_parsers", "icon": "Compass"},
{"display_name": "Prototypes", "name": "prototypes", "icon": "FlaskConical"},
{"display_name": "Retrievers", "name": "retrievers", "icon": "FileSearch"},
{"display_name": "Text Splitters", "name": "textsplitters", "icon": "Scissors"},
{"display_name": "Toolkits", "name": "toolkits", "icon": "Package2"},
{"display_name": "Tools", "name": "tools", "icon": "Hammer"},
]

View file

@ -23,14 +23,3 @@ async def test_get_config(client: AsyncClient):
assert "auto_saving" in result, "The dictionary must contain a key called 'auto_saving'"
assert "health_check_max_retries" in result, "The dictionary must contain a 'health_check_max_retries' key"
assert "max_file_size_upload" in result, "The dictionary must contain a key called 'max_file_size_upload'"
async def test_get_sidebar_components(client: AsyncClient):
response = await client.get("api/v1/sidebar_categories")
result = response.json()
assert response.status_code == status.HTTP_200_OK
assert isinstance(result, dict), "The result must be a dictionary"
assert "categories" in result, "The dictionary must contain a key called 'categories'"
assert len(result["categories"]) > 0, "The categories list must not be empty"
assert isinstance(result["categories"], list), "The categories must be a list"

View file

@ -14,7 +14,8 @@
"@million/lint": "^1.0.0-rc.26",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0",

View file

@ -9,7 +9,8 @@
"@million/lint": "^1.0.0-rc.26",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0",

View file

@ -9,7 +9,7 @@ export const GithubStarComponent = () => {
<FaGithub className="h-4 w-4 text-black dark:text-[white]" />
<div className="hidden text-black dark:text-[white] lg:block">Star</div>
<div className="header-github-display text-black dark:text-[white]">
{stars.toLocaleString() ?? 0}
{stars?.toLocaleString() ?? 0}
</div>
</div>
);

View file

@ -15,13 +15,15 @@ const badgeVariants = cva(
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-primary/80 border-ring/60",
secondaryStatic: "bg-input text-muted-foreground",
pinkStatic: "bg-accent-pink text-accent-pink-foreground",
},
size: {
sm: "h-4 text-xs",
md: "h-5 text-sm",
lg: "h-6 text-base",
sq: "h-6 text-sm font-normal rounded-md",
xq: "h-5 text-xs font-normal rounded-md",
sq: "h-6 px-1.5 text-sm font-medium rounded-md",
xq: "h-5 px-1 text-xs font-medium rounded-md",
},
},
defaultVariants: {

View file

@ -18,8 +18,10 @@ const buttonVariants = cva(
"border bg-background text-secondary-foreground hover:bg-muted dark:hover:bg-muted hover:shadow-sm",
secondary:
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost: "hover:bg-accent hover:text-accent-foreground",
menu: "hover:bg-muted hover:text-accent-foreground focus-visible:!ring-offset-0",
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground",
ghostActive:
"bg-muted text-foreground hover:bg-accent hover:text-accent-foreground",
menu: "hover:bg-muted hover:text-accent-foreground focus:!ring-0 focus-visible:!ring-0",
"menu-active":
"font-semibold hover:bg-muted hover:text-accent-foreground focus-visible:!ring-offset-0",
link: "underline-offset-4 hover:underline text-primary",
@ -29,7 +31,9 @@ const buttonVariants = cva(
sm: "h-9 px-3 rounded-md",
xs: "py-0.5 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "py-1 px-1 rounded-md",
iconMd: "p-1.5 rounded-md",
icon: "p-1 rounded-md",
iconSm: "p-0.5 rounded-md",
"node-toolbar": "py-[6px] px-[6px] rounded-md",
},
},

View file

@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleContent, CollapsibleTrigger };

View file

@ -0,0 +1,191 @@
"use client";
import {
AnimatePresence,
motion,
MotionConfig,
Transition,
Variant,
Variants,
} from "framer-motion";
import * as React from "react";
import { createContext, useContext, useEffect, useId, useState } from "react";
import { cn } from "../../utils/utils";
type DisclosureContextType = {
open: boolean;
toggle: () => void;
variants?: { expanded: Variant; collapsed: Variant };
};
const DisclosureContext = createContext<DisclosureContextType | undefined>(
undefined,
);
type DisclosureProviderProps = {
children: React.ReactNode;
open: boolean;
onOpenChange?: (open: boolean) => void;
variants?: { expanded: Variant; collapsed: Variant };
};
function DisclosureProvider({
children,
open: openProp,
onOpenChange,
variants,
}: DisclosureProviderProps) {
const [internalOpenValue, setInternalOpenValue] = useState<boolean>(openProp);
useEffect(() => {
setInternalOpenValue(openProp);
}, [openProp]);
const toggle = () => {
const newOpen = !internalOpenValue;
setInternalOpenValue(newOpen);
if (onOpenChange) {
onOpenChange(newOpen);
}
};
return (
<DisclosureContext.Provider
value={{
open: internalOpenValue,
toggle,
variants,
}}
>
{children}
</DisclosureContext.Provider>
);
}
function useDisclosure() {
const context = useContext(DisclosureContext);
if (!context) {
throw new Error("useDisclosure must be used within a DisclosureProvider");
}
return context;
}
type DisclosureProps = {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
className?: string;
variants?: { expanded: Variant; collapsed: Variant };
transition?: Transition;
};
export function Disclosure({
open: openProp = false,
onOpenChange,
children,
className,
transition,
variants,
}: DisclosureProps) {
return (
<MotionConfig transition={transition}>
<div className={className}>
<DisclosureProvider
open={openProp}
onOpenChange={onOpenChange}
variants={variants}
>
{React.Children.toArray(children)[0]}
{React.Children.toArray(children)[1]}
</DisclosureProvider>
</div>
</MotionConfig>
);
}
export function DisclosureTrigger({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
const { toggle, open } = useDisclosure();
return (
<>
{React.Children.map(children, (child) => {
return React.isValidElement(child)
? React.cloneElement(child, {
onClick: toggle,
role: "button",
"aria-expanded": open,
tabIndex: 0,
onKeyDown: (e: { key: string; preventDefault: () => void }) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle();
}
},
className: cn(
className,
(child as React.ReactElement).props.className,
),
...(child as React.ReactElement).props,
})
: child;
})}
</>
);
}
export function DisclosureContent({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
const { open, variants } = useDisclosure();
const uniqueId = useId();
const BASE_VARIANTS: Variants = {
expanded: {
height: "auto",
opacity: 1,
},
collapsed: {
height: 0,
opacity: 0,
},
};
const combinedVariants = {
expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
};
return (
<div className={cn("overflow-hidden", className)}>
<AnimatePresence initial={false}>
{open && (
<motion.div
id={uniqueId}
initial="collapsed"
animate="expanded"
exit="collapsed"
variants={combinedVariants}
>
{children}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default {
Disclosure,
DisclosureProvider,
DisclosureTrigger,
DisclosureContent,
};

View file

@ -65,7 +65,7 @@ const SheetContent = React.forwardRef<
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>

View file

@ -5,10 +5,12 @@ import { VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import * as React from "react";
import { useIsMobile } from "../../hooks/use-mobile";
import { cn } from "../../utils/utils";
import { Button } from "./button";
import { Input } from "./input";
import { Separator } from "./separator";
import { Sheet, SheetContent } from "./sheet";
import { Skeleton } from "./skeleton";
import {
Tooltip,
@ -20,13 +22,17 @@ import {
const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "19rem";
const SIDEBAR_WIDTH_ICON = "4rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
@ -47,7 +53,6 @@ const SidebarProvider = React.forwardRef<
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
width?: string;
}
>(
(
@ -58,11 +63,13 @@ const SidebarProvider = React.forwardRef<
className,
style,
children,
width = SIDEBAR_WIDTH,
...props
},
ref,
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
@ -85,8 +92,10 @@ const SidebarProvider = React.forwardRef<
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return setOpen((open) => !open);
}, [setOpen]);
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
@ -113,9 +122,20 @@ const SidebarProvider = React.forwardRef<
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, toggleSidebar],
[
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
],
);
return (
@ -124,7 +144,7 @@ const SidebarProvider = React.forwardRef<
<div
style={
{
"--sidebar-width": width,
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
@ -164,33 +184,47 @@ const Sidebar = React.forwardRef<
},
ref,
) => {
const { state } = useSidebar();
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn(
"group flex h-full w-[--sidebar-width] flex-col bg-background text-foreground",
"flex h-full w-[--sidebar-width] flex-col bg-background text-foreground",
className,
)}
data-side={side}
ref={ref}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col group-data-[side=left]:border-r group-data-[side=right]:border-l"
>
{children}
</div>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-background p-0 text-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer relative block h-full flex-col"
className="group peer relative flex h-full flex-col md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
@ -205,16 +239,11 @@ const Sidebar = React.forwardRef<
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
// Keep icon width on mobile when collapsed
"max-sm:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"absolute inset-y-0 z-50 flex h-full transition-[left,right,width] duration-200 ease-linear",
// Adjust width based on state and device
"w-[--sidebar-width]",
"max-sm:group-data-[state=collapsed]:w-[--sidebar-width-icon]",
"absolute inset-y-0 z-10 hidden h-full w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@ -222,20 +251,13 @@ const Sidebar = React.forwardRef<
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
// Position absolute relative to parent container on mobile
"max-sm:absolute max-sm:h-[100%] max-sm:group-data-[state=expanded]:bg-background/80",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className={cn(
"flex h-full w-full flex-col bg-background",
"group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-border group-data-[variant=floating]:shadow",
// Add shadow on mobile
"max-sm:shadow-lg",
)}
className="flex h-full w-full flex-col bg-background group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-border group-data-[variant=floating]:shadow"
>
{children}
</div>
@ -258,7 +280,7 @@ const SidebarTrigger = React.forwardRef<
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-8 w-8", className)}
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
@ -310,7 +332,7 @@ const SidebarInset = React.forwardRef<
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:m-2 peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 peer-data-[variant=inset]:ml-0 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:shadow",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
@ -328,7 +350,7 @@ const SidebarInput = React.forwardRef<
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-1 focus-visible:ring-ring",
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-ring",
className,
)}
{...props}
@ -426,7 +448,7 @@ const SidebarGroupLabel = React.forwardRef<
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-foreground/70 outline-none ring-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-1 [&>svg]:size-4 [&>svg]:shrink-0",
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-foreground/70 outline-none ring-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
@ -447,9 +469,9 @@ const SidebarGroupAction = React.forwardRef<
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-foreground outline-none ring-ring transition-transform hover:bg-accent hover:text-accent-foreground focus-visible:ring-1 [&>svg]:size-4 [&>svg]:shrink-0",
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-foreground outline-none ring-ring transition-transform hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:-inset-2 after:hidden",
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
@ -499,12 +521,11 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-ring transition-[width,height,padding] hover:bg-accent hover:text-accent-foreground focus-visible:ring-1 active:bg-accent active:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:hover:text-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-ring transition-[width,height,padding] hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 active:bg-accent active:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:hover:text-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default:
"text-secondary-foreground hover:bg-accent hover:text-accent-foreground ",
default: "hover:bg-accent hover:text-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--border))] hover:bg-accent hover:text-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--accent))]",
},
@ -542,7 +563,7 @@ const SidebarMenuButton = React.forwardRef<
ref,
) => {
const Comp = asChild ? Slot : "button";
const { state } = useSidebar();
const { isMobile, state } = useSidebar();
const button = (
<Comp
@ -571,7 +592,7 @@ const SidebarMenuButton = React.forwardRef<
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed"}
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
@ -594,9 +615,9 @@ const SidebarMenuAction = React.forwardRef<
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-foreground outline-none ring-ring transition-transform hover:bg-accent hover:text-accent-foreground focus-visible:ring-1 peer-hover/menu-button:text-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-foreground outline-none ring-ring transition-transform hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:-inset-2 after:hidden",
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
@ -710,7 +731,7 @@ const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-foreground outline-none ring-ring hover:bg-accent hover:text-accent-foreground focus-visible:ring-1 active:bg-accent active:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-accent-foreground",
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-foreground outline-none ring-ring hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 active:bg-accent active:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-accent-foreground",
"data-[active=true]:bg-accent data-[active=true]:text-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",

View file

@ -908,7 +908,7 @@ export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV =
Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) -
Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) * 0.1;
export const TEXT_FIELD_TYPES: string[] = ["str", "SecretStr"];
export const NODE_WIDTH = 400;
export const NODE_WIDTH = 384;
export const NODE_HEIGHT = NODE_WIDTH * 3;
export const SHORTCUT_KEYS = ["cmd", "ctrl", "alt", "shift"];

View file

@ -21,6 +21,7 @@ export const URLs = {
VALIDATE: `validate`,
CONFIG: `config`,
STARTER_PROJECTS: `starter-projects`,
SIDEBAR_CATEGORIES: `sidebar_categories`,
} as const;
export function getURL(key: keyof typeof URLs, params: any = {}) {

View file

@ -1,22 +1,21 @@
import { useEffect, useState } from "react";
import * as React from "react";
export function useMobile(breakpoint: number = 768) {
const [isMobile, setIsMobile] = useState(false);
const MOBILE_BREAKPOINT = 768;
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
// Check initially
checkMobile();
// Add event listener
window.addEventListener("resize", checkMobile);
// Cleanup
return () => window.removeEventListener("resize", checkMobile);
}, [breakpoint]);
return isMobile;
return !!isMobile;
}

View file

@ -0,0 +1,70 @@
import { NODE_WIDTH } from "@/constants/constants";
import { track } from "@/customization/utils/analytics";
import useFlowStore from "@/stores/flowStore";
import { APIClassType } from "@/types/api";
import { NodeType } from "@/types/flow";
import { getNodeId } from "@/utils/reactflowUtils";
import { getNodeRenderType } from "@/utils/utils";
import { useCallback } from "react";
import { useStoreApi } from "reactflow";
export function useAddComponent() {
const store = useStoreApi();
const paste = useFlowStore((state) => state.paste);
const addComponent = useCallback(
(
component: APIClassType,
type: string,
position?: { x: number; y: number },
) => {
track("Component Added", { componentType: component.display_name });
const {
height,
width,
transform: [transformX, transformY, zoomLevel],
} = store.getState();
const zoomMultiplier = 1 / zoomLevel;
let pos;
if (position) {
pos = position;
} else {
let centerX, centerY;
centerX = -transformX * zoomMultiplier + (width * zoomMultiplier) / 2;
centerY = -transformY * zoomMultiplier + (height * zoomMultiplier) / 2;
const nodeOffset = NODE_WIDTH / 2;
pos = {
x: -nodeOffset,
y: -nodeOffset,
paneX: centerX,
paneY: centerY,
};
}
const newId = getNodeId(type);
const newNode: NodeType = {
id: newId,
type: getNodeRenderType("genericnode"),
position: { x: 0, y: 0 },
data: {
node: component,
type: type,
id: newId,
},
};
paste({ nodes: [newNode], edges: [] }, pos);
},
[store, paste],
);
return addComponent;
}

View file

@ -9,11 +9,11 @@ const AstraSVG = (props) => (
>
<path
d="M60.2338 0.25H0.000244141V67.75H60.2338L75.365 56.0752V11.9248L60.2338 0.25ZM11.6732 11.9248H63.692V56.0874H11.6732V11.9248Z"
fill={props.isDark ? "#ffffff" : "#0A0A0A"}
fill={props.isdark ? "#ffffff" : "#0A0A0A"}
/>
<path
d="M162.038 12.415V1H106.964L92.0097 12.415V28.088L106.964 39.503H154.962V55.585H94.9883V67H151.546L166.5 55.585V39.503L151.546 28.088H103.547V12.415H162.038Z"
fill={props.isDark ? "#ffffff" : "#0A0A0A"}
fill={props.isdark ? "#ffffff" : "#0A0A0A"}
/>
</svg>
);

View file

@ -6,6 +6,6 @@ export const AstraDBIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
const isDark = useDarkStore((state) => state.dark);
return <AstraSVG ref={ref} isDark={isDark} {...props} />;
const isdark = useDarkStore((state) => state.dark);
return <AstraSVG ref={ref} isdark={isdark} {...props} />;
});

View file

@ -6,6 +6,7 @@ export const SvgAzure = (props) => (
height="35"
version="1"
viewBox="0 0 750 750"
{...props}
>
<defs>
<filter id="ac4759936b" width="100%" height="100%" x="0%" y="0%">

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
const SvgGroqLogo = ({ color, ...props }) => (
const SvgGroqLogo = ({ ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-label="groq logo"
@ -7,7 +7,8 @@ const SvgGroqLogo = ({ color, ...props }) => (
height="48"
viewBox="0 0 24 24"
fill="none"
class="fill-foreground"
className="fill-foreground"
{...props}
>
<path
fill="#F55036"

View file

@ -11,11 +11,11 @@ const HCDSVG = (props) => (
{/* <rect width="96" height="96" rx="6" fill="white"/> */}
<path
d="M38.0469 33H12V62.1892H38.0469L44.5902 57.1406V38.0485L38.0469 33ZM17.0478 38.0485H39.5424V57.1459H17.0478V38.0485Z"
fill={props.isDark ? "#ffffff" : "#0A0A0A"}
fill={props.isdark ? "#ffffff" : "#0A0A0A"}
/>
<path
d="M82.0705 38.2605V33.3243H58.2546L51.788 38.2605V45.038L58.2546 49.9742H79.0107V56.9286H53.076V61.8648H77.5334L84 56.9286V49.9742L77.5334 45.038H56.7772V38.2605H82.0705Z"
fill={props.isDark ? "#ffffff" : "#0A0A0A"}
fill={props.isdark ? "#ffffff" : "#0A0A0A"}
/>
</g>
<defs>

View file

@ -4,8 +4,8 @@ import HCDSVG from "./HCD";
export const HCDIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
const isDark = useDarkStore((state) => state.dark);
const isdark = useDarkStore((state) => state.dark);
return <HCDSVG ref={ref} isDark={isDark} {...props} />;
return <HCDSVG ref={ref} isdark={isdark} {...props} />;
},
);

View file

@ -1,10 +1,10 @@
export const SvgOllama = (props) => (
<svg
width="646"
height="854"
viewBox="0 0 646 854"
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="40"
height="40"
version="1"
viewBox="0 0 550 850"
{...props}
>
<path

View file

@ -6,6 +6,7 @@ const SvgMistralIcon = (props) => (
viewBox="0 0 256 233"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...props}
>
<title>Mistral AI</title>
<g>

View file

@ -11,12 +11,13 @@ import {
SidebarMenuItem,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { useMobile } from "@/hooks/use-mobile";
import { useIsMobile } from "../../../../hooks/use-mobile";
import { cn } from "@/utils/utils";
import { NavProps } from "../../../../types/templates/types";
export function Nav({ categories, currentTab, setCurrentTab }: NavProps) {
const isMobile = useMobile();
const isMobile = useIsMobile();
return (
<Sidebar collapsible={isMobile ? "icon" : "none"}>

View file

@ -14,8 +14,9 @@ import { useGetBuildsQuery } from "@/controllers/API/queries/_builds";
import { track } from "@/customization/utils/analytics";
import useAutoSaveFlow from "@/hooks/flows/use-autosave-flow";
import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { nodeColors, nodeColorsName } from "@/utils/styleUtils";
import { getNodeRenderType, isSupportedNodeTypes } from "@/utils/utils";
import { useAddComponent } from "@/hooks/useAddComponent";
import { nodeColorsName } from "@/utils/styleUtils";
import { isSupportedNodeTypes } from "@/utils/utils";
import _, { cloneDeep } from "lodash";
import {
KeyboardEvent,
@ -120,6 +121,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
const [isAddingNote, setIsAddingNote] = useState(false);
const [isHighlightingCursor, setIsHighlightingCursor] = useState(false);
const addComponent = useAddComponent();
const { zoomIn, zoomOut, fitView } = useReactFlow();
const { zoom } = useViewport();
@ -439,23 +441,10 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
event.dataTransfer.getData(datakey!),
);
track("Component Added", { componentType: data.node?.display_name });
const newId = getNodeId(data.type);
const newNode: NodeType = {
id: newId,
type: getNodeRenderType(datakey!),
position: { x: 0, y: 0 },
data: {
...data,
id: newId,
},
};
paste(
{ nodes: [newNode], edges: [] },
{ x: event.clientX, y: event.clientY },
);
addComponent(data.node!, data.type, {
x: event.clientX,
y: event.clientY,
});
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
const position = {
@ -478,8 +467,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
});
}
},
// Specify dependencies for useCallback
[getNodeId, setNodes, takeSnapshot, paste],
[takeSnapshot, addComponent],
);
const onEdgeUpdateStart = useCallback(() => {
@ -596,7 +584,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
};
return (
<div className="h-full w-full" ref={reactFlowWrapper}>
<div className="h-full w-full bg-canvas" ref={reactFlowWrapper}>
{showCanvas ? (
<div id="react-flow-id" className="h-full w-full bg-canvas">
<ReactFlow

View file

@ -12,7 +12,7 @@ export function SidebarFilterComponent({
resetFilters: () => void;
}) {
return (
<div className="mb-0.5 flex w-full items-center justify-between rounded border bg-muted p-1 px-2 text-xs font-medium text-primary">
<div className="mb-0.5 flex w-full items-center justify-between rounded border bg-accent-indigo p-2 text-sm text-accent-indigo-foreground">
<div className="flex flex-1 items-center gap-1.5">
<ForwardedIconComponent
name="ListFilter"
@ -30,7 +30,12 @@ export function SidebarFilterComponent({
styleClasses="max-w-full"
content="Remove filter"
>
<Button unstyled className="shrink-0" onClick={resetFilters}>
<Button
unstyled
className="shrink-0"
onClick={resetFilters}
data-testid="sidebar-filter-reset"
>
<ForwardedIconComponent
name="X"
className="h-4 w-4 stroke-2"

View file

@ -0,0 +1,218 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
import { useAddComponent } from "@/hooks/useAddComponent";
import { DragEventHandler, forwardRef, useRef, useState } from "react";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../../../components/ui/select-custom";
import { useDarkStore } from "../../../../../../stores/darkStore";
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
import { APIClassType } from "../../../../../../types/api";
import {
createFlowComponent,
downloadNode,
getNodeId,
} from "../../../../../../utils/reactflowUtils";
import { cn, removeCountFromString } from "../../../../../../utils/utils";
export const SidebarDraggableComponent = forwardRef(
(
{
sectionName,
display_name,
icon,
itemName,
error,
color,
onDragStart,
apiClass,
official,
beta,
legacy,
}: {
sectionName: string;
apiClass: APIClassType;
icon: string;
display_name: string;
itemName: string;
error: boolean;
color: string;
onDragStart: DragEventHandler<HTMLDivElement>;
official: boolean;
beta: boolean;
legacy: boolean;
},
ref,
) => {
const [open, setOpen] = useState(false);
const { deleteFlow } = useDeleteFlow();
const flows = useFlowsManagerStore((state) => state.flows);
const addComponent = useAddComponent();
const version = useDarkStore((state) => state.version);
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const popoverRef = useRef<HTMLDivElement>(null);
const handlePointerDown = (e) => {
if (!open) {
const rect = popoverRef.current?.getBoundingClientRect() ?? {
left: 0,
top: 0,
};
setCursorPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}
};
function handleSelectChange(value: string) {
switch (value) {
case "download":
const type = removeCountFromString(itemName);
downloadNode(
createFlowComponent(
{ id: getNodeId(type), type, node: apiClass },
version,
),
);
break;
case "delete":
const flowId = flows?.find((f) => f.name === display_name);
if (flowId) deleteFlow({ id: flowId.id });
break;
}
}
const handleKeyDown = (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
addComponent(apiClass, itemName);
}
};
return (
<Select
onValueChange={handleSelectChange}
onOpenChange={(change) => setOpen(change)}
open={open}
key={itemName}
>
<div
onPointerDown={handlePointerDown}
onContextMenuCapture={(e) => {
e.preventDefault();
setOpen(true);
}}
key={itemName}
data-tooltip-id={itemName}
tabIndex={0}
onKeyDown={handleKeyDown}
className="rounded-md outline-none ring-ring focus-visible:ring-2"
>
<div
data-testid={sectionName + display_name}
id={sectionName + display_name}
className={cn(
"group/draggable flex cursor-grab items-center gap-2 rounded-md bg-muted p-3 hover:bg-accent",
error ? "cursor-not-allowed select-none" : "",
)}
draggable={!error}
style={{
borderLeftColor: color,
}}
onDragStart={onDragStart}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0],
);
}}
>
<ForwardedIconComponent name={icon} className="h-5 w-5 shrink-0" />
<div className="flex flex-1 items-center overflow-hidden">
<span className="truncate text-sm font-semibold">
{display_name}
</span>
{beta && (
<Badge
variant="pinkStatic"
size="sq"
className="ml-1.5 shrink-0"
>
BETA
</Badge>
)}
{legacy && (
<Badge
variant="secondaryStatic"
size="sq"
className="ml-1.5 shrink-0"
>
LEGACY
</Badge>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
variant="ghost"
size="icon"
tabIndex={-1}
className="text-muted-foreground hover:text-primary group-focus/draggable:text-primary"
onClick={() => addComponent(apiClass, itemName)}
>
<ForwardedIconComponent
name="Plus"
className="h-4 w-4 shrink-0 opacity-0 transition-all group-hover/draggable:opacity-100 group-focus/draggable:opacity-100"
/>
</Button>
<div ref={popoverRef}>
<ForwardedIconComponent
name="GripVertical"
className="h-4 w-4 shrink-0 text-muted-foreground"
/>
<SelectTrigger tabIndex={-1}></SelectTrigger>
<SelectContent
position="popper"
side="bottom"
sideOffset={-25}
style={{
position: "absolute",
left: cursorPos.x,
top: cursorPos.y,
}}
>
<SelectItem value={"download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
{!official && (
<SelectItem value={"delete"}>
<div className="flex">
<IconComponent
name="Trash2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Delete{" "}
</div>{" "}
</SelectItem>
)}
</SelectContent>
</div>
</div>
</div>
</div>
</Select>
);
},
);
export default SidebarDraggableComponent;

View file

@ -0,0 +1,708 @@
import Fuse from "fuse.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; // Import useHotkeys
import ForwardedIconComponent from "@/components/genericIconComponent";
import ShadTooltip from "@/components/shadTooltipComponent";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Disclosure,
DisclosureContent,
DisclosureTrigger,
} from "@/components/ui/disclosure";
import { Input } from "@/components/ui/input";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
} from "@/components/ui/sidebar";
import { Switch } from "@/components/ui/switch";
import { CustomLink } from "@/customization/components/custom-link";
import { useAddComponent } from "@/hooks/useAddComponent";
import { useStoreStore } from "@/stores/storeStore";
import {
nodeColors,
SIDEBAR_BUNDLES,
SIDEBAR_CATEGORIES,
} from "@/utils/styleUtils";
import { removeCountFromString } from "@/utils/utils";
import { cloneDeep } from "lodash";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
import { SidebarFilterComponent } from "../extraSidebarComponent/sidebarFilterComponent";
import sensitiveSort from "../extraSidebarComponent/utils/sensitive-sort";
import ShortcutDisplay from "../nodeToolbarComponent/shortcutDisplay";
import SidebarDraggableComponent from "./components/sidebarDraggableComponent";
export function FlowSidebarComponent() {
const [isInputFocused, setIsInputFocused] = useState(false);
const searchInputRef = useRef<HTMLInputElement | null>(null);
useHotkeys("/", (event) => {
event.preventDefault();
searchInputRef.current?.focus();
});
// Add this new useHotkeys hook
useHotkeys(
"esc",
(event) => {
event.preventDefault();
searchInputRef.current?.blur();
},
{
// Only enable this hotkey when the input is focused
enableOnFormTags: true,
enabled: isInputFocused,
},
);
const categories = SIDEBAR_CATEGORIES;
const bundles = SIDEBAR_BUNDLES;
const data = useTypesStore((state) => state.data);
const templates = useTypesStore((state) => state.templates);
const getFilterEdge = useFlowStore((state) => state.getFilterEdge);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const hasStore = useStoreStore((state) => state.hasStore);
const filterType = useFlowStore((state) => state.filterType);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const addComponent = useAddComponent();
const [fuse, setFuse] = useState<Fuse<any> | null>(null);
const [openCategories, setOpenCategories] = useState<string[]>([]);
const [showConfig, setShowConfig] = useState(false);
const [showBeta, setShowBeta] = useState(true);
const [showLegacy, setShowLegacy] = useState(false);
const hasResults = useMemo(() => {
return Object.values(dataFilter).some(
(category) => Object.keys(category).length > 0,
);
}, [dataFilter]);
useEffect(() => {
filterComponents();
}, [data, search, filterType, getFilterEdge, showBeta, showLegacy]);
function normalizeString(str: string): string {
return str.toLowerCase().replace(/_/g, " ").replace(/\s+/g, "");
}
function searchInMetadata(metadata: any, searchTerm: string): boolean {
if (!metadata || typeof metadata !== "object") return false;
return Object.entries(metadata).some(([key, value]) => {
if (typeof value === "string") {
return (
normalizeString(key).includes(searchTerm) ||
normalizeString(value).includes(searchTerm)
);
}
if (typeof value === "object") {
return searchInMetadata(value, searchTerm);
}
return false;
});
}
const filterComponents = () => {
let filteredData = cloneDeep(data);
// Apply search filter
if (search && fuse) {
const results = fuse.search(search);
filteredData = Object.fromEntries(
Object.entries(data).map(([category, items]) => {
const categoryResults = results.filter(
(result) => result.item.category === category,
);
const filteredItems = Object.fromEntries(
categoryResults.map((result) => [result.item.key, result.item]),
);
return [category, filteredItems];
}),
);
} else {
// Fallback to traditional search if Fuse.js is not available
const searchTerm = normalizeString(search);
filteredData = Object.fromEntries(
Object.entries(data).map(([category, items]) => {
const filteredItems = Object.fromEntries(
Object.entries(items).filter(
([key, item]) =>
normalizeString(key).includes(searchTerm) ||
normalizeString(item.display_name).includes(searchTerm) ||
normalizeString(category).includes(searchTerm) ||
(item.metadata && searchInMetadata(item.metadata, searchTerm)),
),
);
return [category, filteredItems];
}),
);
}
// Apply edge filter
if (getFilterEdge?.length > 0) {
filteredData = Object.fromEntries(
Object.entries(filteredData).map(([family, familyData]) => {
const edgeFilter = getFilterEdge.find((x) => x.family === family);
if (!edgeFilter) return [family, {}];
const filteredTypes = edgeFilter.type
.split(",")
.map((t) => t.trim())
.filter((t) => t !== "");
if (filteredTypes.length === 0) return [family, familyData];
const filteredFamilyData = Object.fromEntries(
Object.entries(familyData).filter(([key]) =>
filteredTypes.includes(key),
),
);
return [family, filteredFamilyData];
}),
);
}
// Apply beta filter
if (!showBeta) {
filteredData = Object.fromEntries(
Object.entries(filteredData).map(([category, items]) => [
category,
Object.fromEntries(
Object.entries(items).filter(([_, value]) => !value.beta),
),
]),
);
}
// Apply legacy filter
if (!showLegacy) {
filteredData = Object.fromEntries(
Object.entries(filteredData).map(([category, items]) => [
category,
Object.fromEntries(
Object.entries(items).filter(([_, value]) => !value.legacy),
),
]),
);
}
setFilterData(filteredData);
if (search !== "" || filterType || getFilterEdge.length > 0) {
setOpenCategories(
Object.keys(filteredData).filter(
(cat) => Object.keys(filteredData[cat]).length > 0,
),
);
}
};
useEffect(() => {
if (search === "" && getFilterEdge.length === 0) {
setOpenCategories([]);
}
}, [search, getFilterEdge]);
function handleSearchInput(e: string) {
setSearch(e);
filterComponents();
}
function onDragStart(
event: React.DragEvent<any>,
data: { type: string; node?: APIClassType },
): void {
//start drag event
var crt = event.currentTarget.cloneNode(true);
crt.style.position = "absolute";
crt.style.width = "215px";
crt.style.top = "-500px";
crt.style.right = "-500px";
crt.classList.add("cursor-grabbing");
document.body.appendChild(crt);
event.dataTransfer.setDragImage(crt, 0, 0);
event.dataTransfer.setData("genericNode", JSON.stringify(data));
}
useEffect(() => {
// show components with error on load
let errors: string[] = [];
Object.keys(templates).forEach((component) => {
if (templates[component].error) {
errors.push(component);
}
});
if (errors.length > 0)
setErrorData({ title: " Components with errors: ", list: errors });
}, []);
useEffect(() => {
if (getFilterEdge.length !== 0) {
setSearch("");
}
}, [getFilterEdge, data]);
useEffect(() => {
const options = {
keys: ["display_name", "description", "type"],
threshold: 0.3,
};
const fuseData = Object.entries(data).flatMap(([category, items]) =>
Object.entries(items).map(([key, value]) => ({
...value,
category,
key,
})),
);
setFuse(new Fuse(fuseData, options));
handleSearchInput(search);
}, [data]);
const customComponent = useMemo(() => {
return data?.["custom_component"]?.["CustomComponent"] ?? null;
}, [data]);
const handleKeyDown = (
e: React.KeyboardEvent<HTMLDivElement>,
name: string,
) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setOpenCategories((prev) =>
prev.includes(name)
? prev.filter((cat) => cat !== name)
: [...prev, name],
);
}
};
return (
<Sidebar data-testid="shad-sidebar">
<SidebarHeader className="flex w-full flex-col gap-4 p-4 pb-1">
<Disclosure open={showConfig} onOpenChange={setShowConfig}>
<div className="flex w-full items-center justify-between">
<h3 className="text-sm font-semibold">Components</h3>
<DisclosureTrigger>
<Button
variant={showConfig ? "ghostActive" : "ghost"}
size="iconMd"
data-testid="sidebar-options-trigger"
>
<ForwardedIconComponent
name="SlidersHorizontal"
className="h-4 w-4"
/>
</Button>
</DisclosureTrigger>
</div>
<DisclosureContent>
<div className="flex flex-col gap-7 border-b pb-7 pt-5">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium">
Show{" "}
<Badge variant="pinkStatic" size="sq">
BETA
</Badge>{" "}
Components
</span>
</div>
<Switch
checked={showBeta}
onCheckedChange={setShowBeta}
data-testid="sidebar-beta-switch"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium">
Show{" "}
<Badge variant="secondaryStatic" size="sq">
LEGACY
</Badge>{" "}
Components
</span>
</div>
<Switch
checked={showLegacy}
onCheckedChange={setShowLegacy}
data-testid="sidebar-legacy-switch"
/>
</div>
</div>
</DisclosureContent>
</Disclosure>
<div className="relative w-full flex-1">
<ForwardedIconComponent
name="Search"
className="absolute inset-y-0 left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-primary"
/>
<Input
ref={searchInputRef}
type="search"
data-testid="sidebar-search-input"
className="w-full rounded-lg bg-background pl-8 text-sm"
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
value={search}
onChange={(e) => handleSearchInput(e.target.value)}
/>
{!isInputFocused && search === "" && (
<div className="pointer-events-none absolute inset-y-0 left-8 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm text-muted-foreground">
Type{" "}
<span>
<ShortcutDisplay sidebar shortcut="/" />
</span>{" "}
to search components
</div>
)}
</div>
{filterType && (
<SidebarFilterComponent
isInput={!!filterType.source}
type={filterType.type}
resetFilters={() => {
setFilterEdge([]);
setFilterData(data);
}}
/>
)}
</SidebarHeader>
<SidebarContent className="p-2">
{hasResults ? (
<>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{!data
? Array.from({ length: 5 }).map((_, index) => (
<SidebarMenuItem key={index}>
<SidebarMenuSkeleton />
</SidebarMenuItem>
))
: categories.map(
(item) =>
dataFilter[item.name] &&
Object.keys(dataFilter[item.name]).length > 0 && (
<Collapsible
key={item.name}
className="group/collapsible"
open={openCategories.includes(item.name)}
onOpenChange={(isOpen) => {
setOpenCategories((prev) =>
isOpen
? [...prev, item.name]
: prev.filter((cat) => cat !== item.name),
);
}}
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<div
data-testid={`disclosure-${item.display_name.toLocaleLowerCase()}`}
tabIndex={0}
onKeyDown={(e) =>
handleKeyDown(e, item.name)
}
className="flex cursor-pointer items-center gap-2"
>
<ForwardedIconComponent
name={item.icon}
className="h-4 w-4 text-muted-foreground group-data-[state=open]/collapsible:text-pink-600 group-data-[state=open]/collapsible:dark:text-pink-400"
/>
<span className="group-data-[state=open]/collapsible:font-semibold">
{item.display_name}
</span>
<ForwardedIconComponent
name="ChevronRight"
className="h-4 w-4 text-muted-foreground transition-all group-data-[state=open]/collapsible:rotate-90"
/>
</div>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1 py-2">
{Object.keys(dataFilter[item.name])
.sort((a, b) =>
sensitiveSort(
dataFilter[item.name][a].display_name,
dataFilter[item.name][b].display_name,
),
)
.map((SBItemName: string, idx) => (
<ShadTooltip
content={
dataFilter[item.name][SBItemName]
.display_name
}
side="right"
key={idx}
>
<SidebarDraggableComponent
sectionName={item.name as string}
apiClass={
dataFilter[item.name][SBItemName]
}
icon={
dataFilter[item.name][SBItemName]
.icon ??
item.icon ??
"Unknown"
}
key={idx}
onDragStart={(event) =>
onDragStart(event, {
type: removeCountFromString(
SBItemName,
),
node: dataFilter[item.name][
SBItemName
],
})
}
color={nodeColors[item.name]}
itemName={SBItemName}
error={
!!dataFilter[item.name][
SBItemName
].error
}
display_name={
dataFilter[item.name][SBItemName]
.display_name
}
official={
dataFilter[item.name][SBItemName]
.official === false
? false
: true
}
beta={
dataFilter[item.name][SBItemName]
.beta ?? false
}
legacy={
dataFilter[item.name][SBItemName]
.legacy ?? false
}
/>
</ShadTooltip>
))}
</div>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
),
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Bundles</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{!data
? Array.from({ length: 5 }).map((_, index) => (
<SidebarMenuItem key={index}>
<SidebarMenuSkeleton />
</SidebarMenuItem>
))
: bundles.map(
(item) =>
dataFilter[item.name] &&
Object.keys(dataFilter[item.name]).length > 0 && (
<Collapsible
key={item.name}
className="group/collapsible"
open={openCategories.includes(item.name)}
onOpenChange={(isOpen) => {
setOpenCategories((prev) =>
isOpen
? [...prev, item.name]
: prev.filter((cat) => cat !== item.name),
);
}}
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<div
tabIndex={0}
onKeyDown={(e) =>
handleKeyDown(e, item.name)
}
className="flex cursor-pointer items-center gap-2"
>
<ForwardedIconComponent
name={item.icon}
className="h-4 w-4 text-muted-foreground group-data-[state=open]/collapsible:text-pink-600 group-data-[state=open]/collapsible:dark:text-pink-400"
/>
<span className="group-data-[state=open]/collapsible:font-semibold">
{item.display_name}
</span>
<ForwardedIconComponent
name="ChevronRight"
className="h-4 w-4 text-muted-foreground transition-all group-data-[state=open]/collapsible:rotate-90"
/>
</div>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1 py-2">
{Object.keys(dataFilter[item.name])
.sort((a, b) =>
sensitiveSort(
dataFilter[item.name][a].display_name,
dataFilter[item.name][b].display_name,
),
)
.map((SBItemName: string, idx) => (
<ShadTooltip
content={
dataFilter[item.name][SBItemName]
.display_name
}
side="right"
key={idx}
>
<SidebarDraggableComponent
sectionName={item.name as string}
apiClass={
dataFilter[item.name][SBItemName]
}
icon={
dataFilter[item.name][SBItemName]
.icon ??
item.icon ??
"Unknown"
}
key={idx}
onDragStart={(event) =>
onDragStart(event, {
type: removeCountFromString(
SBItemName,
),
node: dataFilter[item.name][
SBItemName
],
})
}
color={nodeColors[item.name]}
itemName={SBItemName}
error={
!!dataFilter[item.name][
SBItemName
].error
}
display_name={
dataFilter[item.name][SBItemName]
.display_name
}
official={
dataFilter[item.name][SBItemName]
.official === false
? false
: true
}
beta={
dataFilter[item.name][SBItemName]
.beta ?? false
}
legacy={
dataFilter[item.name][SBItemName]
.legacy ?? false
}
/>
</ShadTooltip>
))}
</div>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
),
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</>
) : (
<div className="flex h-full flex-col items-center justify-center p-4 text-center">
<ForwardedIconComponent
name="Search"
className="mb-4 h-8 w-8 text-muted-foreground"
/>
<h3 className="mb-2 text-lg font-semibold">No results found</h3>
<p className="text-sm text-muted-foreground">
Try adjusting your search or filter to find what you're looking
for.
</p>
</div>
)}
</SidebarContent>
<SidebarFooter className="border-t p-4 py-3">
{hasStore && (
<SidebarMenuButton asChild>
<CustomLink to="/store">
<div className="flex items-center gap-2">
<ForwardedIconComponent
name="Store"
className="h-4 w-4 text-muted-foreground"
/>
<span className="group-data-[state=open]/collapsible:font-semibold">
Discover more components
</span>
</div>
</CustomLink>
</SidebarMenuButton>
)}
<SidebarMenuButton asChild>
<Button
unstyled
onClick={() => {
if (customComponent) {
addComponent(customComponent, "CustomComponent");
}
}}
data-testid="sidebar-custom-component-button"
className="flex items-center gap-2"
>
<ForwardedIconComponent
name="Plus"
className="h-4 w-4 text-muted-foreground"
/>
<span className="group-data-[state=open]/collapsible:font-semibold">
Custom Component
</span>
</Button>
</SidebarMenuButton>
</SidebarFooter>
</Sidebar>
);
}

View file

@ -1,21 +1,40 @@
import RenderIcons from "@/components/renderIconComponent";
import { cn } from "@/utils/utils";
export default function ShortcutDisplay({
name,
shortcut,
sidebar = false,
}: {
name: string;
name?: string;
shortcut: string;
sidebar?: boolean;
}): JSX.Element {
const fixedShortcut = shortcut?.split("+");
return (
<div className="flex content-center items-center justify-center self-center text-[12px]">
<span> {name} </span>
<span
className={`ml-3 flex items-center rounded-sm bg-primary-hover px-1.5 py-[0.1em] text-muted`}
>
<RenderIcons filteredShortcut={fixedShortcut} />
</span>
</div>
<>
{sidebar ? (
<div className="flex justify-center">
{name && <span> {name} </span>}
<span
className={cn(
"flex items-center rounded-sm bg-muted px-1.5 py-[0.1em] text-lg text-muted-foreground",
name && "ml-3",
)}
>
<RenderIcons filteredShortcut={fixedShortcut} />
</span>
</div>
) : (
<div className="flex content-center items-center justify-center self-center text-[12px]">
<span> {name} </span>
<span
className={`ml-3 flex items-center rounded-sm bg-primary-hover px-1.5 py-[0.1em] text-muted`}
>
<RenderIcons filteredShortcut={fixedShortcut} />
</span>
</div>
)}
</>
);
}

View file

@ -1,6 +1,6 @@
import { SidebarProvider } from "@/components/ui/sidebar";
import { useGetFlow } from "@/controllers/API/queries/flows/use-get-flow";
import { useGetRefreshFlows } from "@/controllers/API/queries/flows/use-get-refresh-flows";
import { ENABLE_BRANDING } from "@/customization/feature-flags";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import { SaveChangesModal } from "@/modals/saveChangesModal";
@ -14,7 +14,7 @@ import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import Page from "./components/PageComponent";
import ExtraSidebar from "./components/extraSidebarComponent";
import { FlowSidebarComponent } from "./components/flowSidebarComponent";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
@ -165,16 +165,17 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
<div className="flow-page-positioning">
{currentFlow && (
<div className="flex h-full overflow-hidden">
{!view && <ExtraSidebar />}
<main className="flex flex-1">
{/* Primary column */}
<div className="h-full w-full">
<Page />
</div>
</main>
<SidebarProvider>
{!view && <FlowSidebarComponent />}
<main className="flex flex-1">
<div className="h-full w-full">
<Page />
</div>
</main>
</SidebarProvider>
</div>
)}
{ENABLE_BRANDING && version && (
{/* {ENABLE_BRANDING && version && (
<a
target={"_blank"}
href="https://medium.com/logspace/langflow-datastax-better-together-1b7462cebc4d"
@ -184,7 +185,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
<div className={version ? "mt-2" : "mt-1"}> v{version}</div>
</a>
)}
)} */}
</div>
{blocker.state === "blocked" && (
<>

View file

@ -7,7 +7,6 @@ import {
templatesGenerator,
typesGenerator,
} from "../utils/reactflowUtils";
import useAlertStore from "./alertStore";
import useFlowsManagerStore from "./flowsManagerStore";
export const useTypesStore = create<TypesStoreType>((set, get) => ({

View file

@ -260,8 +260,7 @@ pre {
-webkit-appearance: none;
background-color: hsl(var(--primary));
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23777'><path d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
background-size: 16px 16px;
height: 16px;
opacity: 1 !important;
width: 16px;
background-size: 20px 20px;
height: 20px;
width: 20px;
}

View file

@ -248,6 +248,14 @@
--chat-bot-icon: #235d70;
--chat-user-icon: #4f3d6e;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--emerald-success: 160.1 84.1% 39.4%;
--accent-emerald-foreground: 161.4 93.5% 30.4%;
--placeholder: 240 5% 64.9%;

View file

@ -43,6 +43,7 @@ export type APIClassType = {
output_types?: Array<string>;
custom_fields?: CustomFieldsType;
beta?: boolean;
legacy?: boolean;
documentation: string;
error?: string;
official?: boolean;

View file

@ -0,0 +1,5 @@
export interface SidebarCategory {
display_name: string;
name: string;
icon: string;
}

View file

@ -84,6 +84,7 @@ import {
GitFork,
GithubIcon,
Globe,
GripVertical,
Group,
Hammer,
Heart,
@ -295,7 +296,7 @@ export const gradients = [
"bg-gradient-to-br from-lime-600 via-yellow-300 to-red-600",
];
/*
/*
Specifications
#FF3276 -> #F480FF
#1A0250 -> #2F10FE
@ -416,41 +417,49 @@ export const nodeColorsName: { [char: string]: string } = {
BaseChatMemory: "cyan",
};
export const nodeNames: { [char: string]: string } = {
inputs: "Inputs",
outputs: "Outputs",
data: "Data",
prompts: "Prompts",
models: "Models",
notion: "Notion",
Notion: "Notion",
AssemblyAI: "AssemblyAI",
assemblyai: "AssemblyAI",
model_specs: "Model Specs",
chains: "Chains",
agents: "Agents",
tools: "Tools",
memories: "Memories",
saved_components: "Saved",
advanced: "Advanced",
chat: "Chat",
embeddings: "Embeddings",
documentloaders: "Loaders",
vectorstores: "Vector Stores",
vectorsearch: "Vector Search",
toolkits: "Toolkits",
wrappers: "Wrappers",
textsplitters: "Text Splitters",
retrievers: "Retrievers",
helpers: "Helpers",
prototypes: "Prototypes",
astra_assistants: "Astra Assistants",
langchain_utilities: "Utilities",
output_parsers: "Output Parsers",
custom_components: "Custom",
link_extractors: "Link Extractors",
unknown: "Other",
};
export const SIDEBAR_CATEGORIES = [
{ display_name: "Saved", name: "saved_components", icon: "GradientSave" },
{ display_name: "Inputs", name: "inputs", icon: "Download" },
{ display_name: "Outputs", name: "outputs", icon: "Upload" },
{ display_name: "Prompts", name: "prompts", icon: "TerminalSquare" },
{ display_name: "Data", name: "data", icon: "Database" },
{ display_name: "Models", name: "models", icon: "BrainCircuit" },
{ display_name: "Helpers", name: "helpers", icon: "Wand2" },
{ display_name: "Vector Stores", name: "vectorstores", icon: "Layers" },
{ display_name: "Embeddings", name: "embeddings", icon: "Binary" },
{ display_name: "Agents", name: "agents", icon: "Bot" },
{ display_name: "Chains", name: "chains", icon: "Link" },
{ display_name: "Loaders", name: "documentloaders", icon: "Paperclip" },
{
display_name: "Utilities",
name: "langchain_utilities",
icon: "PocketKnife",
},
{ display_name: "Link Extractors", name: "link_extractors", icon: "Link2" },
{ display_name: "Memories", name: "memories", icon: "Cpu" },
{ display_name: "Output Parsers", name: "output_parsers", icon: "Compass" },
{ display_name: "Prototypes", name: "prototypes", icon: "FlaskConical" },
{ display_name: "Retrievers", name: "retrievers", icon: "FileSearch" },
{ display_name: "Text Splitters", name: "textsplitters", icon: "Scissors" },
{ display_name: "Toolkits", name: "toolkits", icon: "Package2" },
{ display_name: "Tools", name: "tools", icon: "Hammer" },
];
export const SIDEBAR_BUNDLES = [
{ display_name: "CrewAI", name: "crewai", icon: "CrewAi" },
{ display_name: "LangChain", name: "langchain_utilities", icon: "LangChain" },
{ display_name: "AssemblyAI", name: "assemblyai", icon: "AssemblyAI" },
{
display_name: "Astra Assistants",
name: "astra_assistants",
icon: "Sparkles",
},
{ display_name: "Google", name: "google", icon: "Google" },
{ display_name: "Firecrawl", name: "firecrawl", icon: "FirecrawlCrawlApi" },
{ display_name: "Notion", name: "Notion", icon: "Notion" },
{ display_name: "NVIDIA", name: "nvidia", icon: "NVIDIA" },
{ display_name: "Vectara", name: "vectara", icon: "Vectara" },
];
export const categoryIcons = {
saved_components: GradientSave,
@ -669,6 +678,16 @@ export const nodeIconsLucide: iconsType = {
Blocks,
ChevronDown,
ArrowLeft,
BrainCircuit,
Wand2,
Layers,
Binary,
Paperclip,
PocketKnife,
Scissors,
Cpu,
Hammer,
GradientSave,
Shield,
Plus,
Redo,
@ -743,6 +762,10 @@ export const nodeIconsLucide: iconsType = {
Loader2,
BookmarkPlus,
Heart,
Package2,
FileSearch,
Compass,
Link2,
Pin,
Link,
ToyBrick,
@ -768,6 +791,7 @@ export const nodeIconsLucide: iconsType = {
note: StickyNote,
RotateCcw,
Wrench,
GripVertical,
FolderPlusIcon,
PaperclipIcon,
Settings,

View file

@ -0,0 +1,73 @@
import { expect, test } from "@playwright/test";
test("user can add components by hovering and clicking the plus icon", async ({
page,
}) => {
// Navigate to homepage and handle initial modal
await page.goto("/");
await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(3000);
modalCount = await page.getByTestId("modal-title")?.count();
}
// Start with blank flow
await page.getByTestId("blank-flow").click();
await page.waitForTimeout(1000);
// Search for a component
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForTimeout(500);
// Hover over the component and verify plus icon
const componentLocator = page.getByTestId("inputsChat Input");
// Find the plus icon within the specific component container
const plusIcon = componentLocator.getByTestId("icon-Plus");
// Get the opacity
const opacity = await plusIcon.evaluate((el) =>
window.getComputedStyle(el).getPropertyValue("opacity"),
);
await expect(plusIcon).toBeVisible();
await expect(opacity).toBe("0");
// Hover over the component
await componentLocator.hover();
// Check if the plus icon is visible and has full opacity
await expect(plusIcon).toBeVisible();
await page.waitForTimeout(500);
const opacityAfterHover = await plusIcon.evaluate((el) =>
window.getComputedStyle(el).getPropertyValue("opacity"),
);
await expect(opacityAfterHover).toBe("1");
// Click the plus icon associated with this component
await plusIcon.click();
await page.waitForTimeout(500);
// Verify component was added to the flow
const addedComponent = await page.locator(".react-flow__node").first();
await expect(addedComponent).toBeVisible();
});

View file

@ -23,12 +23,14 @@ test("user must see on handle hover a tooltip with possibility connections", asy
}
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("retrievalqa");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("retrievalqa");
await page.getByTestId("sidebar-options-trigger").click();
await page.getByTestId("sidebar-legacy-switch").isVisible({ timeout: 5000 });
await page.getByTestId("sidebar-legacy-switch").click();
await expect(page.getByTestId("sidebar-legacy-switch")).toBeChecked();
await page.getByTestId("sidebar-options-trigger").click();
await page.waitForTimeout(1000);
await page

View file

@ -32,13 +32,8 @@ test("user must see on handle click the possibility connections - LLMChain", asy
await page.getByTestId("blank-flow").click();
await page.waitForTimeout(3000);
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 100000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("api request");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("api request");
await page.waitForTimeout(1000);
await page
@ -77,6 +72,14 @@ test("user must see on handle click the possibility connections - LLMChain", asy
await expect(page.getByTestId("disclosure-models")).toBeVisible();
await expect(page.getByTestId("disclosure-helpers")).toBeVisible();
await expect(page.getByTestId("disclosure-agents")).toBeVisible();
await expect(page.getByTestId("disclosure-chains")).not.toBeVisible();
await page.getByTestId("sidebar-options-trigger").click();
await page.getByTestId("sidebar-legacy-switch").isVisible({ timeout: 5000 });
await page.getByTestId("sidebar-legacy-switch").click();
await expect(page.getByTestId("sidebar-legacy-switch")).toBeChecked();
await page.getByTestId("sidebar-options-trigger").click();
await expect(page.getByTestId("disclosure-chains")).toBeVisible();
await expect(page.getByTestId("disclosure-prototypes")).toBeVisible();
@ -89,7 +92,17 @@ test("user must see on handle click the possibility connections - LLMChain", asy
await expect(page.getByTestId("chainsConversationChain")).toBeVisible();
await expect(page.getByTestId("prototypesConditional Router")).toBeVisible();
await page.getByPlaceholder("Search").click();
await expect(page.getByTestId("helpersCurrent Date")).toBeVisible();
await page.getByTestId("sidebar-options-trigger").click();
await page.getByTestId("sidebar-beta-switch").isVisible({ timeout: 5000 });
await page.getByTestId("sidebar-beta-switch").click();
await expect(page.getByTestId("sidebar-beta-switch")).not.toBeChecked();
await page.getByTestId("sidebar-options-trigger").click();
await expect(page.getByTestId("helpersCurrent Date")).not.toBeVisible();
await page.getByTestId("sidebar-filter-reset").click();
await expect(page.getByTestId("inputsChat Input")).not.toBeVisible();
await expect(page.getByTestId("outputsChat Output")).not.toBeVisible();
@ -117,7 +130,16 @@ test("user must see on handle click the possibility connections - LLMChain", asy
await expect(page.getByTestId("helpersChat Memory")).toBeVisible();
await expect(page.getByTestId("vectorstoresAstra DB")).toBeVisible();
await expect(page.getByTestId("toolsSearch API")).toBeVisible();
await expect(page.getByTestId("prototypesSub Flow")).not.toBeVisible();
await page.getByTestId("sidebar-options-trigger").click();
await page.getByTestId("sidebar-beta-switch").isVisible({ timeout: 5000 });
await page.getByTestId("sidebar-beta-switch").click();
await expect(page.getByTestId("sidebar-beta-switch")).toBeChecked();
await page.getByTestId("sidebar-options-trigger").click();
await expect(page.getByTestId("prototypesSub Flow")).toBeVisible();
await expect(
page.getByTestId("retrieversSelf Query Retriever"),
).toBeVisible();

View file

@ -30,9 +30,8 @@ test.skip("user must be able to freeze a component", async ({ page }) => {
//first component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text input");
await page.waitForTimeout(1000);
await page
@ -52,9 +51,8 @@ test.skip("user must be able to freeze a component", async ({ page }) => {
//second component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("url");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("url");
await page.waitForTimeout(1000);
await page
@ -74,9 +72,8 @@ test.skip("user must be able to freeze a component", async ({ page }) => {
//third component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("split text");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("split text");
await page.waitForTimeout(1000);
await page
@ -96,9 +93,8 @@ test.skip("user must be able to freeze a component", async ({ page }) => {
//fourth component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("parse data");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("parse data");
await page.waitForTimeout(1000);
await page
@ -118,9 +114,8 @@ test.skip("user must be able to freeze a component", async ({ page }) => {
//fifth component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForTimeout(1000);
await page

View file

@ -31,12 +31,8 @@ test("user must be able to save or delete a global variable", async ({
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("openai");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("openai");
await page.waitForTimeout(1000);

View file

@ -0,0 +1,96 @@
import { expect, test } from "@playwright/test";
test("user can search and add components using keyboard shortcuts", async ({
page,
}) => {
// Navigate to homepage and handle initial modal
await page.goto("/");
await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(3000);
modalCount = await page.getByTestId("modal-title")?.count();
}
// Start with blank flow
await page.getByTestId("blank-flow").click();
await page.waitForTimeout(1000);
// Press "/" to activate search
await page.keyboard.press("/");
await page.waitForTimeout(500);
// Verify search is focused and disclosures are closed when search is empty
await expect(page.getByTestId("sidebar-search-input")).toBeFocused();
await expect(page.getByTestId("inputsChat Input")).not.toBeVisible();
// Type "chat" to search for chat components
await page.keyboard.type("chat");
await page.waitForTimeout(500);
// Verify disclosures open when search has content
await expect(page.getByTestId("inputsChat Input")).toBeVisible();
// Press Tab to focus first result
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
// Verify some expected chat-related components are visible
await expect(page.getByTestId("inputsChat Input")).toBeVisible();
await expect(page.getByTestId("outputsChat Output")).toBeVisible();
// Press Space to select the component
await page.keyboard.press("Space");
await page.waitForTimeout(500);
// Verify component was added to flow
const addedComponent = await page.locator(".react-flow__node").first();
await expect(addedComponent).toBeVisible();
// Clear search input and verify disclosures are closed
await page.getByTestId("sidebar-search-input").clear();
await page.waitForTimeout(500);
await expect(page.getByTestId("inputsChat Input")).not.toBeVisible();
// Test Enter key selection
await page.keyboard.press("/");
await page.keyboard.type("prompt");
await page.waitForTimeout(500);
// Verify disclosures open with new search
await expect(page.getByTestId("promptsPrompt")).toBeVisible();
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
await page.waitForTimeout(500);
// Verify second component was added
const nodeCount = await page.locator(".react-flow__node").count();
expect(nodeCount).toBe(2);
// Verify search is cleared and disclosures are closed after adding component
await page.keyboard.press("/");
await page.getByTestId("sidebar-search-input").clear();
await page.waitForTimeout(500);
await expect(page.getByTestId("sidebar-search-input")).toHaveValue("");
await expect(page.getByTestId("inputsChat Input")).not.toBeVisible();
await expect(page.getByTestId("sidebar-search-input")).toBeFocused();
await page.keyboard.press("Escape");
await expect(page.getByTestId("sidebar-search-input")).not.toBeFocused();
await expect(page.getByTestId("inputsChat Input")).not.toBeVisible();
});

View file

@ -38,12 +38,9 @@ test("fresh start playground", async ({ page }) => {
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForTimeout(1000);
await page
@ -52,8 +49,8 @@ test("fresh start playground", async ({ page }) => {
await page.mouse.up();
await page.mouse.down();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForTimeout(1000);
await page

View file

@ -103,12 +103,8 @@ test.describe("save component tests", () => {
if (replaceButton) {
await page.getByTestId("replace-button").click();
}
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("group");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("group");
await page.waitForTimeout(1000);
await page

View file

@ -33,9 +33,8 @@ test("user must be able to stop a building", async ({ page }) => {
//first component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text input");
// await page.waitForTimeout(1000);
await page
@ -55,9 +54,8 @@ test("user must be able to stop a building", async ({ page }) => {
//second component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("url");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("url");
// await page.waitForTimeout(1000);
await page
@ -77,9 +75,8 @@ test("user must be able to stop a building", async ({ page }) => {
//third component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("split text");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("split text");
// await page.waitForTimeout(1000);
await page
@ -99,9 +96,8 @@ test("user must be able to stop a building", async ({ page }) => {
//fourth component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("parse data");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("parse data");
// await page.waitForTimeout(1000);
await page
@ -121,9 +117,8 @@ test("user must be able to stop a building", async ({ page }) => {
//fifth component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
// await page.waitForTimeout(1000);
await page
@ -248,17 +243,7 @@ class CustomComponent(Component):
return data
`;
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("custom component");
await page.waitForTimeout(1000);
await page
.locator('//*[@id="helpersCustom Component"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.getByTestId("sidebar-custom-component-button").click();
await page.getByTestId("fit_view").click();
await page.getByTestId("zoom_out").click();

View file

@ -88,12 +88,8 @@ test("check if tweaks are updating when someothing on the flow changes", async (
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("Chroma");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("Chroma");
await page.waitForTimeout(1000);

View file

@ -1,83 +1,150 @@
import { expect, test } from "@playwright/test";
import { Page, test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
// Helper function to wait for element to be ready
async function waitForElement(page: Page, elementId: string, nth: number) {
const element = page.getByTestId(`title-${elementId}`).nth(nth);
// Add this function at the beginning of the file, after the imports
// Wait for element to be visible and stable
await element.waitFor({
state: "visible",
timeout: 30000,
});
// Additional wait to ensure element is fully rendered and interactive
await page.waitForTimeout(1000);
return element;
}
// Improved version of moveElementByX with better error handling and waits
async function moveElementByX(
page: any,
page: Page,
elementId: string,
moveX: number,
nth: number,
) {
const element = await page.getByTestId(`title-${elementId}`).nth(nth);
await element.hover();
try {
const element = await waitForElement(page, elementId, nth);
await element.hover();
const boundingBox = await element.boundingBox();
const boundingBox = await element.boundingBox();
if (!boundingBox) {
throw new Error(
`Unable to get bounding box for the element: ${elementId}`,
);
}
if (boundingBox) {
const startX = boundingBox.x + boundingBox.width / 2;
const startY = boundingBox.y + boundingBox.height / 2;
// Break down mouse movements into smaller steps for more reliability
await page.mouse.move(startX, startY);
await page.waitForTimeout(50);
await page.mouse.down();
await page.mouse.move(startX + moveX, startY);
await page.waitForTimeout(50);
// Move in smaller increments
const steps = 3;
const stepX = moveX / steps;
for (let i = 1; i <= steps; i++) {
await page.mouse.move(startX + stepX * i, startY);
await page.waitForTimeout(50);
}
await page.mouse.up();
} else {
throw new Error(`Unable to get bounding box for the element: ${elementId}`);
await page.waitForTimeout(100);
} catch (error) {
console.error(`Failed to move element ${elementId}:`, error);
throw error;
}
}
// Add this function at the beginning of the file, after the imports
// Improved version of moveElementByY with better error handling and waits
async function moveElementByY(
page: any,
page: Page,
elementId: string,
moveY: number,
nth: number,
) {
const element = await page.getByTestId(`title-${elementId}`).nth(nth);
await element.hover();
try {
const element = await waitForElement(page, elementId, nth);
await element.hover();
const boundingBox = await element.boundingBox();
const boundingBox = await element.boundingBox();
if (!boundingBox) {
throw new Error(
`Unable to get bounding box for the element: ${elementId}`,
);
}
if (boundingBox) {
const startX = boundingBox.x + boundingBox.width / 2;
const startY = boundingBox.y + boundingBox.height / 2;
await page.mouse.move(startX, startY);
await page.waitForTimeout(100);
await page.mouse.down();
await page.mouse.move(startX, startY + moveY);
await page.waitForTimeout(100);
// Move in smaller increments
const steps = 5;
const stepY = moveY / steps;
for (let i = 1; i <= steps; i++) {
await page.mouse.move(startX, startY + stepY * i);
await page.waitForTimeout(50);
}
await page.mouse.up();
} else {
throw new Error(`Unable to get bounding box for the element: ${elementId}`);
await page.waitForTimeout(100);
} catch (error) {
console.error(`Failed to move element ${elementId}:`, error);
throw error;
}
}
// Add this function at the beginning of the file, after the imports
// Improved version of moveElementByXY with better error handling and waits
async function moveElementByXY(
page: any,
page: Page,
elementId: string,
moveX: number,
moveY: number,
nth: number,
) {
const element = await page.getByTestId(`title-${elementId}`).nth(nth);
await element.hover();
try {
const element = await waitForElement(page, elementId, nth);
await element.hover();
const boundingBox = await element.boundingBox();
const boundingBox = await element.boundingBox();
if (!boundingBox) {
throw new Error(
`Unable to get bounding box for the element: ${elementId}`,
);
}
if (boundingBox) {
const startX = boundingBox.x + boundingBox.width / 2;
const startY = boundingBox.y + boundingBox.height / 2;
await page.mouse.move(startX, startY);
await page.waitForTimeout(100);
await page.mouse.down();
await page.mouse.move(startX + moveX, startY + moveY);
await page.waitForTimeout(100);
// Move in smaller increments
const steps = 5;
const stepX = moveX / steps;
const stepY = moveY / steps;
for (let i = 1; i <= steps; i++) {
await page.mouse.move(startX + stepX * i, startY + stepY * i);
await page.waitForTimeout(50);
}
await page.mouse.up();
} else {
throw new Error(`Unable to get bounding box for the element: ${elementId}`);
await page.waitForTimeout(100);
} catch (error) {
console.error(`Failed to move element ${elementId}:`, error);
throw error;
}
}
test("should create a flow with decision", async ({ page }) => {
test.skip(
!process?.env?.OPENAI_API_KEY,
@ -111,21 +178,17 @@ test("should create a flow with decision", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
//---------------------------------- CHAT INPUT
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForTimeout(500);
await page
.getByTestId("inputsChat Input")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
//---------------------------------- CREATE LIST
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("list");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("list");
await page.waitForTimeout(500);
await page
.getByTestId("helpersCreate List")
@ -158,8 +221,8 @@ test("should create a flow with decision", async ({ page }) => {
await page.getByTestId("inputlist_str_texts_2").last().fill("not cool..");
//---------------------------------- PARSE DATA
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("parse data");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("parse data");
await page.waitForTimeout(500);
await page
@ -170,8 +233,8 @@ test("should create a flow with decision", async ({ page }) => {
.dragTo(page.locator('//*[@id="react-flow-id"]'));
//---------------------------------- PASS
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("pass");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("pass");
await page.waitForTimeout(500);
await page
.getByTestId("prototypesPass")
@ -185,30 +248,30 @@ test("should create a flow with decision", async ({ page }) => {
.getByTestId("prototypesPass")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
//---------------------------------- PROMPT
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("prompt");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("prompt");
await page.waitForTimeout(500);
await page
.getByTestId("promptsPrompt")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
//---------------------------------- OPENAI
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("openai");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("openai");
await page.waitForTimeout(500);
await page
.getByTestId("modelsOpenAI")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
//---------------------------------- CONDITIONAL ROUTER
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("conditional router");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("conditional router");
await page.waitForTimeout(500);
await page
.getByTestId("prototypesConditional Router")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
//---------------------------------- CHAT OUTPUT
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForTimeout(500);
await page
.getByTestId("outputsChat Output")
@ -222,32 +285,42 @@ test("should create a flow with decision", async ({ page }) => {
await page.getByTestId("fit_view").click();
await moveElementByX(page, "Chat Output", 500, 1);
await page.waitForTimeout(500);
await moveElementByX(page, "Chat Output", 1000, 0);
await moveElementByX(page, "Chat Output", 400, 1);
await page.waitForTimeout(500);
await moveElementByX(page, "Conditional Router", 1500, 0);
await moveElementByX(page, "Chat Output", 700, 0);
await page.waitForTimeout(500);
await moveElementByX(page, "OpenAI", 2000, 0);
await moveElementByX(page, "Conditional Router", 1000, 0);
await page.waitForTimeout(500);
await moveElementByX(page, "Prompt", 2500, 0);
await page.getByTestId("fit_view").click();
await moveElementByX(page, "OpenAI", 980, 0);
await page.getByTestId("fit_view").click();
await page.waitForTimeout(500);
await moveElementByX(page, "Pass", 3000, 2);
await moveElementByX(page, "Prompt", 990, 0);
await page.getByTestId("fit_view").click();
await page.waitForTimeout(500);
await moveElementByX(page, "Pass", 1000, 2);
await page.getByTestId("fit_view").click();
await page.waitForTimeout(500);
await moveElementByXY(page, "Pass", 0, 200, 1);
await page.getByTestId("fit_view").click();
await page.waitForTimeout(500);
await moveElementByXY(page, "Pass", 150, 200, 0);
await page.getByTestId("fit_view").click();
await page.waitForTimeout(500);
await moveElementByXY(page, "Parse Data", 300, 200, 1);
await page.getByTestId("fit_view").click();
await page.waitForTimeout(500);
await moveElementByXY(page, "Parse Data", 450, 200, 0);
await page.waitForTimeout(500);
await moveElementByXY(page, "Create List", 600, 200, 1);
await page.waitForTimeout(500);
await moveElementByXY(page, "Create List", 800, 200, 0);
await page.waitForTimeout(500);
await moveElementByXY(page, "Chat Input", 1000, 200, 0);
await page.waitForTimeout(500);
await page.getByTestId("fit_view").click();

View file

@ -31,9 +31,8 @@ test("user must be able to check similarity between embedding texts", async ({
//first component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("openai");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("openai");
// await page.waitForTimeout(1000);
await page
@ -70,9 +69,8 @@ test("user must be able to check similarity between embedding texts", async ({
//third component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text embedder");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text embedder");
// await page.waitForTimeout(1000);
await page
@ -109,9 +107,8 @@ test("user must be able to check similarity between embedding texts", async ({
//fifth component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("embedding similarity");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("embedding similarity");
// await page.waitForTimeout(1000);
await page
@ -131,9 +128,8 @@ test("user must be able to check similarity between embedding texts", async ({
//sisxth component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("parse data");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("parse data");
// await page.waitForTimeout(1000);
await page
@ -153,9 +149,8 @@ test("user must be able to check similarity between embedding texts", async ({
//seventh component
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
// await page.waitForTimeout(1000);
await page
@ -173,9 +168,8 @@ test("user must be able to check similarity between embedding texts", async ({
await page.mouse.up();
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("filter data");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("filter data");
// await page.waitForTimeout(1000);
await page

View file

@ -41,19 +41,9 @@ test("TextInputOutputComponent", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
const focusElementsOnBoard = async ({ page }) => {
const focusElements = await page.getByTestId("extended-disclosure");
focusElements.click();
};
await focusElementsOnBoard({ page });
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text input");
await page.waitForTimeout(1000);
await page
@ -62,8 +52,8 @@ test("TextInputOutputComponent", async ({ page }) => {
await page.mouse.up();
await page.mouse.down();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("openai");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("openai");
await page.waitForTimeout(1000);
await page
@ -120,8 +110,8 @@ test("TextInputOutputComponent", async ({ page }) => {
// Release the mouse
await page.mouse.up();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
await page
.getByTestId("outputsText Output")

View file

@ -30,13 +30,9 @@ test("should be able to see output preview from grouped components and connect c
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text input");
await page.waitForTimeout(1000);
await page
@ -61,9 +57,8 @@ test("should be able to see output preview from grouped components and connect c
.getByTestId("inputsText Input")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("combine text");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("combine text");
await page.waitForTimeout(1000);
await page
@ -107,9 +102,8 @@ test("should be able to see output preview from grouped components and connect c
await page.mouse.up();
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
await page.waitForTimeout(1000);
await page

View file

@ -38,9 +38,8 @@ test("memory should work as expect", async ({ page }) => {
await page.getByTestId("fit_view").click();
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat memory");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat memory");
// Locate the canvas element
const canvas = page.locator("#react-flow-id"); // Update the selector if needed

View file

@ -32,12 +32,8 @@ test("chat_io_teste", async ({ page }) => {
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForTimeout(1000);
await page
@ -46,8 +42,8 @@ test("chat_io_teste", async ({ page }) => {
await page.mouse.up();
await page.mouse.down();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("chat input");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForTimeout(1000);
await page

View file

@ -31,12 +31,8 @@ test("CodeAreaModalComponent", async ({ page }) => {
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("python function");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("python function");
await page.waitForTimeout(1000);

View file

@ -30,13 +30,9 @@ test("dropDownComponent", async ({ page }) => {
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("amazon");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("amazon");
await page.waitForTimeout(1000);

View file

@ -30,13 +30,9 @@ test("should be able to upload a file", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("file");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("file");
await page.waitForTimeout(1000);
@ -58,8 +54,8 @@ test("should be able to upload a file", async ({ page }) => {
);
await page.getByText("test_file.txt").isVisible();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("text output");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
await page
.getByTestId("outputsText Output")
@ -72,8 +68,8 @@ test("should be able to upload a file", async ({ page }) => {
await page.getByTestId("zoom_out").click();
await page.getByTestId("zoom_out").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("parse data");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("parse data");
await page
.getByTestId("helpersParse Data")
.first()

View file

@ -29,12 +29,8 @@ test("FloatComponent", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("ollama");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("ollama");
await page.waitForTimeout(1000);

View file

@ -29,12 +29,8 @@ test("InputComponent", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("Chroma");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("Chroma");
await page.waitForTimeout(1000);

View file

@ -21,12 +21,8 @@ test("InputListComponent", async ({ page }) => {
}
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("url");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("url");
await page.waitForTimeout(1000);
await page

View file

@ -29,12 +29,8 @@ test("IntComponent", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("openai");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("openai");
await page.waitForTimeout(1000);

View file

@ -29,12 +29,8 @@ test("KeypairListComponent", async ({ page }) => {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("amazon bedrock");
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("amazon bedrock");
await page.waitForTimeout(1000);

Some files were not shown because too many files have changed in this diff Show more