merge fix

This commit is contained in:
cristhianzl 2024-02-26 14:24:34 -03:00
commit 652780a6ee
33 changed files with 757 additions and 467 deletions

View file

@ -1,32 +0,0 @@
from langchain.llms.base import BaseLLM
from langchain.prompts import PromptTemplate
from langchain_core.messages import BaseMessage
from langflow import CustomComponent
from langflow.field_typing import Text
class PromptRunner(CustomComponent):
display_name: str = "Prompt Runner"
description: str = "Run a Chain with the given PromptTemplate"
beta: bool = True
field_config = {
"llm": {"display_name": "LLM"},
"prompt": {
"display_name": "Prompt Template",
"info": "Make sure the prompt has all variables filled.",
},
"code": {"show": False},
}
def build(self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}) -> Text:
chain = prompt | llm
# The input is an empty dict because the prompt is already filled
result_message: BaseMessage = chain.invoke(input=inputs)
if hasattr(result_message, "content"):
result: str = result_message.content
elif isinstance(result_message, str):
result = result_message
else:
result = str(result_message)
self.repr_value = result
return result

View file

@ -1,25 +0,0 @@
from langflow import CustomComponent
from typing import Callable, Union
from langflow.field_typing import BasePromptTemplate, BaseLanguageModel, Chain
from langchain_community.utilities.sql_database import SQLDatabase
from langchain_experimental.sql.base import SQLDatabaseChain
class SQLDatabaseChainComponent(CustomComponent):
display_name = "SQLDatabaseChain"
description = ""
def build_config(self):
return {
"db": {"display_name": "Database"},
"llm": {"display_name": "LLM"},
"prompt": {"display_name": "Prompt"},
}
def build(
self,
db: SQLDatabase,
llm: BaseLanguageModel,
prompt: BasePromptTemplate,
) -> Union[Chain, Callable, SQLDatabaseChain]:
return SQLDatabaseChain.from_llm(llm=llm, db=db, prompt=prompt)

View file

@ -0,0 +1,62 @@
from typing import Optional
from langchain.chains import create_sql_query_chain
from langchain_community.utilities.sql_database import SQLDatabase
from langchain_core.prompts import PromptTemplate
from langflow import CustomComponent
from langflow.field_typing import BaseLanguageModel, Text
class SQLGeneratorComponent(CustomComponent):
display_name = "Natural Language to SQL"
description = "Generate SQL from natural language."
def build_config(self):
return {
"db": {"display_name": "Database"},
"llm": {"display_name": "LLM"},
"prompt": {
"display_name": "Prompt",
"info": "The prompt must contain `{question}`.",
},
"top_k": {
"display_name": "Top K",
"info": "The number of results per select statement to return. If 0, no limit.",
},
}
def build(
self,
inputs: Text,
db: SQLDatabase,
llm: BaseLanguageModel,
top_k: int = 5,
prompt: Optional[PromptTemplate] = None,
) -> Text:
if top_k > 0:
kwargs = {
"k": top_k,
}
if not prompt:
sql_query_chain = create_sql_query_chain(llm=llm, db=db, **kwargs)
else:
template = prompt.template if hasattr(prompt, "template") else prompt
# Check if {question} is in the prompt
if (
"{question}" not in template
or "question" not in template.input_variables
):
raise ValueError(
"Prompt must contain `{question}` to be used with Natural Language to SQL."
)
sql_query_chain = create_sql_query_chain(
llm=llm, db=db, prompt=prompt, **kwargs
)
query_writer = sql_query_chain | {
"query": lambda x: x.replace("SQLQuery:", "").strip()
}
response = query_writer.invoke({"question": inputs})
query = response.get("query")
self.status = query
return query

View file

@ -1,7 +1,8 @@
from typing import Optional
from langflow import CustomComponent
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
from langchain.llms.base import BaseLLM
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
from langflow import CustomComponent
class HuggingFaceEndpointsComponent(CustomComponent):
@ -31,11 +32,11 @@ class HuggingFaceEndpointsComponent(CustomComponent):
model_kwargs: Optional[dict] = None,
) -> BaseLLM:
try:
output = HuggingFaceEndpoint(
output = HuggingFaceEndpoint( # type: ignore
endpoint_url=endpoint_url,
task=task,
huggingfacehub_api_token=huggingfacehub_api_token,
model_kwargs=model_kwargs,
model_kwargs=model_kwargs or {},
)
except Exception as e:
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e

View file

@ -0,0 +1,22 @@
from langchain_experimental.sql.base import SQLDatabase
from langflow import CustomComponent
class SQLDatabaseComponent(CustomComponent):
display_name = "SQLDatabase"
description = "SQL Database"
def build_config(self):
return {
"uri": {"display_name": "URI", "info": "URI to the database."},
}
def clean_up_uri(self, uri: str) -> str:
if uri.startswith("postgresql://"):
uri = uri.replace("postgresql://", "postgres://")
return uri.strip()
def build(self, uri: str) -> SQLDatabase:
uri = self.clean_up_uri(uri)
return SQLDatabase.from_uri(uri)

View file

@ -0,0 +1,56 @@
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
from langchain_experimental.sql.base import SQLDatabase
from langflow import CustomComponent
from langflow.field_typing import Text
class SQLExecutorComponent(CustomComponent):
display_name = "SQL Executor"
description = "Execute SQL query."
def build_config(self):
return {
"database": {"display_name": "Database"},
"include_columns": {
"display_name": "Include Columns",
"info": "Include columns in the result.",
},
"passthrough": {
"display_name": "Passthrough",
"info": "If an error occurs, return the query instead of raising an exception.",
},
"add_error": {
"display_name": "Add Error",
"info": "Add the error to the result.",
},
}
def build(
self,
query: str,
database: SQLDatabase,
include_columns: bool = False,
passthrough: bool = False,
add_error: bool = False,
) -> Text:
error = None
try:
tool = QuerySQLDataBaseTool(db=database)
result = tool.run(query, include_columns=include_columns)
self.status = result
except Exception as e:
result = str(e)
self.status = result
if not passthrough:
raise e
error = repr(e)
if add_error and error is not None:
result = f"{result}\n\nError: {error}\n\nQuery: {query}"
elif error is not None:
# Then we won't add the error to the result
# but since we are in passthrough mode, we will return the query
result = query
return result

View file

@ -85,7 +85,9 @@ class Graph:
def build_parent_child_map(self):
parent_child_map = defaultdict(list)
for vertex in self.vertices:
parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)]
parent_child_map[vertex.id] = [
child.id for child in self.get_successors(vertex)
]
return parent_child_map
def increment_run_count(self):
@ -149,6 +151,18 @@ class Graph:
# both graphs have the same vertices and edges
# but the data of the vertices might be different
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
return vertex.__repr__() == other_vertex.__repr__()
def vertex_edges_are_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
same_length = len(vertex.edges) == len(other_vertex.edges)
if not same_length:
return False
for edge in vertex.edges:
if edge not in other_vertex.edges:
return False
return True
def update(self, other: "Graph") -> None:
# Existing vertices in self graph
existing_vertex_ids = set(vertex.id for vertex in self.vertices)
@ -161,11 +175,14 @@ class Graph:
# Find vertices that are in self but not in other (removed vertices)
removed_vertex_ids = existing_vertex_ids - other_vertex_ids
# Create a set for new edges
new_edges = set()
# Update existing vertices that have changed
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
self_vertex = self.get_vertex(vertex_id)
other_vertex = other.get_vertex(vertex_id)
if self_vertex.__repr__() != other_vertex.__repr__():
if not self.vertex_data_is_identical(self_vertex, other_vertex):
self_vertex._data = other_vertex._data
self_vertex._parse_data()
self_vertex.params = {}
@ -179,6 +196,14 @@ class Graph:
self_vertex.artifacts = None
self_vertex.set_top_level(self.top_level_vertices)
self.reset_all_edges_of_vertex(self_vertex)
if not self.vertex_edges_are_identical(self_vertex, other_vertex):
new_edges.update(other_vertex.edges)
# Add new edges
# to self.edges if they are not already in self.edges
for edge in new_edges:
if edge not in self.edges:
self.edges.append(edge)
# Remove vertices
for vertex_id in removed_vertex_ids:
@ -255,7 +280,11 @@ class Graph:
return
self.vertices.remove(vertex)
self.vertex_map.pop(vertex_id)
self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id]
self.edges = [
edge
for edge in self.edges
if edge.source_id != vertex_id and edge.target_id != vertex_id
]
def _build_vertex_params(self) -> None:
"""Identifies and handles the LLM vertex within the graph."""
@ -276,7 +305,9 @@ class Graph:
return
for vertex in self.vertices:
if not self._validate_vertex(vertex):
raise ValueError(f"{vertex.vertex_type} is not connected to any other components")
raise ValueError(
f"{vertex.vertex_type} is not connected to any other components"
)
def _validate_vertex(self, vertex: Vertex) -> bool:
"""Validates a vertex."""
@ -292,7 +323,11 @@ class Graph:
def get_vertex_edges(self, vertex_id: str) -> List[ContractEdge]:
"""Returns a list of edges for a given vertex."""
return [edge for edge in self.edges if edge.source_id == vertex_id or edge.target_id == vertex_id]
return [
edge
for edge in self.edges
if edge.source_id == vertex_id or edge.target_id == vertex_id
]
def get_vertices_with_target(self, vertex_id: str) -> List[Vertex]:
"""Returns the vertices connected to a vertex."""
@ -330,7 +365,9 @@ class Graph:
def dfs(vertex):
if state[vertex] == 1:
# We have a cycle
raise ValueError("Graph contains a cycle, cannot perform topological sort")
raise ValueError(
"Graph contains a cycle, cannot perform topological sort"
)
if state[vertex] == 0:
state[vertex] = 1
for edge in vertex.edges:
@ -354,11 +391,17 @@ class Graph:
def get_predecessors(self, vertex):
"""Returns the predecessors of a vertex."""
return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
return [
self.get_vertex(source_id)
for source_id in self.predecessor_map.get(vertex.id, [])
]
def get_successors(self, vertex):
"""Returns the successors of a vertex."""
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])]
return [
self.get_vertex(target_id)
for target_id in self.successor_map.get(vertex.id, [])
]
def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]:
"""Returns the neighbors of a vertex."""
@ -397,7 +440,9 @@ class Graph:
edges.append(ContractEdge(source, target, edge))
return edges
def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]:
def _get_vertex_class(
self, node_type: str, node_base_type: str, node_id: str
) -> Type[Vertex]:
"""Returns the node class based on the node type."""
# First we check for the node_base_type
node_name = node_id.split("-")[0]
@ -428,14 +473,18 @@ class Graph:
vertex_type: str = vertex_data["type"] # type: ignore
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore
VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"])
VertexClass = self._get_vertex_class(
vertex_type, vertex_base_type, vertex_data["id"]
)
vertex_instance = VertexClass(vertex, graph=self)
vertex_instance.set_top_level(self.top_level_vertices)
vertices.append(vertex_instance)
return vertices
def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]:
def get_children_by_vertex_type(
self, vertex: Vertex, vertex_type: str
) -> List[Vertex]:
"""Returns the children of a vertex based on the vertex type."""
children = []
vertex_types = [vertex.data["type"]]
@ -447,7 +496,9 @@ class Graph:
def __repr__(self):
vertex_ids = [vertex.id for vertex in self.vertices]
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
edges_repr = "\n".join(
[f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]
)
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
def sort_up_to_vertex(self, vertex_id: str) -> "Graph":
@ -478,7 +529,9 @@ class Graph:
"""Performs a layered topological sort of the vertices in the graph."""
# Queue for vertices with no incoming edges
queue = deque(vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0)
queue = deque(
vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0
)
layers = []
current_layer = 0
@ -534,7 +587,9 @@ class Graph:
return refined_layers
def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
def sort_chat_inputs_first(
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
chat_inputs_first = []
for layer in vertices_layers:
for vertex_id in layer:
@ -562,11 +617,15 @@ class Graph:
self.increment_run_count()
return vertices_layers
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
def sort_interface_components_first(
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
def contains_interface_component(vertex):
return any(component.value in vertex for component in InterfaceComponentTypes)
return any(
component.value in vertex for component in InterfaceComponentTypes
)
# Sort each inner list so that vertices containing ChatInput or ChatOutput come first
sorted_vertices = [
@ -585,9 +644,13 @@ class Graph:
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
if len(vertices_ids) == 1:
return vertices_ids
vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time)
vertices_ids.sort(
key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time
)
return vertices_ids
sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers]
sorted_vertices = [
sort_layer_by_avg_build_time(layer) for layer in vertices_layers
]
return sorted_vertices

View file

@ -27,14 +27,18 @@ from langflow.utils import validate
from langflow.utils.util import get_base_classes
def add_output_types(frontend_node: CustomComponentFrontendNode, return_types: List[str]):
def add_output_types(
frontend_node: CustomComponentFrontendNode, return_types: List[str]
):
"""Add output types to the frontend node"""
for return_type in return_types:
if return_type is None:
raise HTTPException(
status_code=400,
detail={
"error": ("Invalid return type. Please check your code and try again."),
"error": (
"Invalid return type. Please check your code and try again."
),
"traceback": traceback.format_exc(),
},
)
@ -63,14 +67,18 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List
frontend_node.template.fields = reordered_fields
def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: List[str]):
def add_base_classes(
frontend_node: CustomComponentFrontendNode, return_types: List[str]
):
"""Add base classes to the frontend node"""
for return_type_instance in return_types:
if return_type_instance is None:
raise HTTPException(
status_code=400,
detail={
"error": ("Invalid return type. Please check your code and try again."),
"error": (
"Invalid return type. Please check your code and try again."
),
"traceback": traceback.format_exc(),
},
)
@ -145,10 +153,14 @@ def add_new_custom_field(
# If options is a list, then it's a dropdown
# If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list)
field_config["is_list"] = is_list or field_config.get("is_list", False) or field_contains_list
field_config["is_list"] = (
is_list or field_config.get("is_list", False) or field_contains_list
)
if "name" in field_config:
warnings.warn("The 'name' key in field_config is used to build the object and can't be changed.")
warnings.warn(
"The 'name' key in field_config is used to build the object and can't be changed."
)
required = field_config.pop("required", field_required)
placeholder = field_config.pop("placeholder", "")
@ -179,7 +191,9 @@ def add_extra_fields(frontend_node, field_config, function_args):
if "name" not in extra_field or extra_field["name"] == "self":
continue
field_name, field_type, field_value, field_required = get_field_properties(extra_field)
field_name, field_type, field_value, field_required = get_field_properties(
extra_field
)
config = field_config.get(field_name, {})
frontend_node = add_new_custom_field(
frontend_node,
@ -217,7 +231,9 @@ def run_build_config(
raise HTTPException(
status_code=400,
detail={
"error": ("Invalid type convertion. Please check your code and try again."),
"error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(),
},
) from exc
@ -245,7 +261,9 @@ def run_build_config(
raise HTTPException(
status_code=400,
detail={
"error": ("Invalid type convertion. Please check your code and try again."),
"error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(),
},
) from exc
@ -300,16 +318,24 @@ def build_custom_component_template(
frontend_node = build_frontend_node(custom_component.template_config)
logger.debug("Updated attributes")
field_config, custom_instance = run_build_config(custom_component, user_id=user_id, update_field=update_field)
field_config, custom_instance = run_build_config(
custom_component, user_id=user_id, update_field=update_field
)
logger.debug("Built field config")
entrypoint_args = custom_component.get_function_entrypoint_args
add_extra_fields(frontend_node, field_config, entrypoint_args)
frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {}))
frontend_node = add_code_field(
frontend_node, custom_component.code, field_config.get("code", {})
)
add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type)
add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type)
add_base_classes(
frontend_node, custom_component.get_function_entrypoint_return_type
)
add_output_types(
frontend_node, custom_component.get_function_entrypoint_return_type
)
logger.debug("Added base classes")
reorder_fields(frontend_node, custom_instance._get_field_order())
@ -321,7 +347,9 @@ def build_custom_component_template(
raise HTTPException(
status_code=400,
detail={
"error": ("Invalid type convertion. Please check your code and try again."),
"error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(),
},
) from exc
@ -345,7 +373,9 @@ def build_custom_components(settings_service):
if not settings_service.settings.COMPONENTS_PATH:
return {}
logger.info(f"Building custom components from {settings_service.settings.COMPONENTS_PATH}")
logger.info(
f"Building custom components from {settings_service.settings.COMPONENTS_PATH}"
)
custom_components_from_file = {}
processed_paths = set()
for path in settings_service.settings.COMPONENTS_PATH:
@ -356,7 +386,9 @@ def build_custom_components(settings_service):
custom_component_dict = build_custom_component_list_from_path(path_str)
if custom_component_dict:
category = next(iter(custom_component_dict))
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
logger.info(
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
)
custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict
)
@ -373,7 +405,7 @@ def update_field_dict(field_dict):
field_dict["refresh"] = True
if "value" in field_dict and callable(field_dict["value"]):
field_dict["value"] = field_dict["value"](field_dict.get("options", []))
field_dict["value"] = field_dict["value"]()
field_dict["refresh"] = True
# Let's check if "range_spec" is a RangeSpec object

View file

@ -2,14 +2,14 @@ from cachetools import LRUCache, cached
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.directory_reader.utils import merge_nested_dicts_with_renaming
from langflow.interface.custom.directory_reader.utils import \
merge_nested_dicts_with_renaming
from langflow.interface.custom.utils import build_custom_components
from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator
from langflow.interface.llms.base import llm_creator
from langflow.interface.memories.base import memory_creator
from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.prompts.base import prompt_creator
from langflow.interface.retrievers.base import retriever_creator
from langflow.interface.text_splitters.base import textsplitter_creator
from langflow.interface.toolkits.base import toolkits_creator
@ -39,7 +39,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
creators = [
chain_creator,
agent_creator,
prompt_creator,
# prompt_creator,
llm_creator,
memory_creator,
tool_creator,

View file

@ -1,6 +1,6 @@
from typing import ClassVar, Dict, List, Optional
from typing import Dict, List, Optional
from langchain_community.utilities import requests, sql_database
from langchain_community.utilities import requests
from loguru import logger
from langflow.interface.base import LangChainTypeCreator
@ -10,13 +10,11 @@ from langflow.utils.util import build_template_from_class, build_template_from_m
class WrapperCreator(LangChainTypeCreator):
type_name: str = "wrappers"
from_method_nodes: ClassVar[Dict] = {"SQLDatabase": "from_uri"}
@property
def type_to_loader_dict(self) -> Dict:
if self.type_dict is None:
self.type_dict = {
wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper, sql_database.SQLDatabase]
wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper]
}
return self.type_dict

View file

@ -68,7 +68,9 @@ class TemplateField(BaseModel):
refresh: Optional[bool] = None
"""Specifies if the field should be refreshed. Defaults to False."""
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
range_spec: Optional[RangeSpec] = Field(
default=None, serialization_alias="rangeSpec"
)
"""Range specification for the field. Defaults to None."""
title_case: bool = False
@ -115,6 +117,10 @@ class TemplateField(BaseModel):
if not isinstance(value, list):
raise ValueError("file_types must be a list")
return [
(f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type)
(
f".{file_type}"
if isinstance(file_type, str) and not file_type.startswith(".")
else file_type
)
for file_type in value
]

View file

@ -44,7 +44,7 @@ export default function GenericNode({
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const buildStatus = useFlowStore((state) =>state.flowBuildStatus[data.id]);
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [handles, setHandles] = useState<number>(0);
@ -122,7 +122,6 @@ export default function GenericNode({
}, [flowPool, data.id]);
const showNode = data.showNode ?? true;
const pinned = data.node?.pinned ?? false;
const nameEditable = data.node?.flow || data.type === "CustomComponent";
@ -162,7 +161,7 @@ export default function GenericNode({
const getIconPlayOrPauseComponent = (name, className) => (
<IconComponent
name={name}
className={`absolute h-5 stroke-2 ${className} ml-0.5`}
className={`h-4 fill-current stroke-2 ${className}`}
/>
);
@ -181,15 +180,18 @@ export default function GenericNode({
return "red-status";
} else if (!validationStatus && buildStatus === BuildStatus.TO_BUILD) {
return "green-status";
} else if (buildStatus === BuildStatus.BUILDING) {
return "status-build-animation";
} else {
return "green-status";
return "yellow-status";
}
};
const isDark = useDarkStore((state) => state.dark);
console.log(isDark);
const renderIconStatusComponents = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
const className = getStatusClassName(buildStatus, validationStatus);
return <>{getIconPlayOrPauseComponent("CircleDot", className)}</>;
};
const renderIconPlayOrPauseComponents = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
@ -197,8 +199,12 @@ export default function GenericNode({
if (buildStatus === BuildStatus.BUILDING) {
return <Loading />;
} else {
const className = getStatusClassName(buildStatus, validationStatus);
return <>{getIconPlayOrPauseComponent("Play", className)}</>;
return (
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground hover:text-medium-indigo"
/>
);
}
};
@ -332,28 +338,37 @@ export default function GenericNode({
</div>
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="flex items-center gap-2"
onDoubleClick={(event) => {
if (nameEditable) {
setInputName(true);
}
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
>
<div
data-testid={"title-" + data.node?.display_name}
className="generic-node-tooltip-div text-primary"
>
{data.node?.display_name}
</div>
<div className="group flex items-center gap-2.5">
<ShadTooltip content={data.node?.display_name}>
<div
onDoubleClick={(event) => {
if (nameEditable) {
setInputName(true);
}
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
data-testid={"title-" + data.node?.display_name}
className="generic-node-tooltip-div text-primary"
>
{data.node?.display_name}
</div>
</ShadTooltip>
{nameEditable && (
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
/>
<div
onClick={(event) => {
setInputName(true);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
>
<IconComponent
name="Pencil"
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block"
/>
</div>
)}
</div>
</ShadTooltip>
@ -451,44 +466,11 @@ export default function GenericNode({
</div>
{showNode && (
<Button
variant="outline"
className="h-9 px-1.5"
onClick={() => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
pinned: old.data?.node?.pinned ? false : true,
},
},
}));
}}
>
<Tooltip
title={<span>{pinned ? "Pin Output" : "Unpin Output"}</span>}
>
<div className="generic-node-status-position flex items-center">
<IconComponent
name={"Pin"}
className={cn(
"h-5 fill-transparent stroke-chat-trigger stroke-2 transition-all",
pinned ? "animate-wiggle fill-chat-trigger" : ""
)}
/>
</div>
</Tooltip>
</Button>
)}
{showNode && (
<Button
variant="outline"
className={"h-9 px-1.5"}
variant="secondary"
className={"group h-9 px-1.5"}
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
buildFlow(data.id);
}}
>
@ -529,6 +511,32 @@ export default function GenericNode({
</div>
</Button>
)}
<div className="">
<Tooltip
title={
data?.buildStatus === BuildStatus.BUILDING ? (
<span>Building...</span>
) : !validationStatus ? (
<span className="flex">Build to validate status.</span>
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? `${durationString}\n${validationStatus.params}`
.split("\n")
.map((line, index) => <div key={index}>{line}</div>)
: durationString}
</div>
)
}
>
<div>
{renderIconStatusComponents(
data?.buildStatus,
validationStatus
)}
</div>
</Tooltip>
</div>
</div>
</div>

View file

@ -63,7 +63,7 @@ export default function IOInputField({
}
}
return (
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
<div className="font-xl flex items-start h-full w-full flex-col gap-4 p-4 font-semibold">
{inputType}
{handleInputType()}
</div>

View file

@ -44,7 +44,7 @@ export default function IOOutputView({
}
}
return (
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
<div className="font-xl flex h-full items-start w-full flex-col gap-4 p-4 font-semibold">
{outputType}
{handleOutputType()}
</div>

View file

@ -1,9 +1,7 @@
import { ReactNode, useState } from "react";
import { useEffect, useState } from "react";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import BaseModal from "../../modals/baseModal";
import useFlowStore from "../../stores/flowStore";
import { NodeType } from "../../types/flow";
import { isInputType, isOutputType } from "../../utils/reactflowUtils";
import { cn } from "../../utils/utils";
import AccordionComponent from "../AccordionComponent";
import IOInputField from "../IOInputField";
@ -12,62 +10,40 @@ import IconComponent from "../genericIconComponent";
import NewChatView from "../newChatView";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
export default function IOView({ children, open, setOpen }): JSX.Element {
const inputs = useFlowStore((state) => state.inputs);
const outputs = useFlowStore((state) => state.outputs);
const inputIds = inputs.map((obj) => obj.id);
const outputIds = outputs.map((obj) => obj.id);
const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode);
const categories = getCategories();
const [selectedCategory, setSelectedCategory] = useState<number>(0);
const [showChat, setShowChat] = useState<boolean>(false);
const [selectedView, setSelectedView] = useState<{
type: string;
id?: string;
}>(handleInitialView());
const inputs = useFlowStore((state) => state.inputs).filter(
(input) => input.type !== "ChatInput"
);
const outputs = useFlowStore((state) => state.outputs).filter(
(output) => output.type !== "ChatOutput"
);
const nodes = useFlowStore((state) => state.nodes).filter(
(node) =>
(inputs.some((input) => input.id === node.id) ||
outputs.some((output) => output.id === node.id)) &&
node.type !== "ChatInput" &&
node.type !== "ChatOutput"
);
const haveChat = useFlowStore((state) => state.outputs).some(
(output) => output.type === "ChatOutput"
);
const [selectedTab, setSelectedTab] = useState(
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
);
const [selectedViewField, setSelectedViewField] = useState<
{ type: string; id: string } | undefined
>(undefined);
type CategoriesType = { name: string; icon: string };
function handleInitialView() {
if (
outputs.map((output) => output.type).includes("ChatOutput") ||
inputs.map((input) => input.type).includes("ChatInput")
) {
return { type: "ChatOutput" };
}
return { type: "" };
}
function getCategories() {
const categories: CategoriesType[] = [];
if (inputs.filter((input) => input.type !== "ChatInput").length > 0)
categories.push({ name: "Inputs", icon: "TextCursorInput" });
if (outputs.filter((output) => output.type !== "ChatOutput").length > 0)
categories.push({ name: "Outputs", icon: "TerminalSquare" });
return categories;
}
function handleSelectChange(): ReactNode {
const { type, id } = selectedView;
if (type === "ChatOutput") return <NewChatView />;
if (isInputType(type))
return <IOInputField inputId={id!} inputType={type} />;
if (isOutputType(type))
return <IOOutputView outputId={id!} outputType={type} />;
else return undefined;
}
function UpdateAccordion() {
return (categories[selectedCategory]?.name ?? "Inputs") === "Inputs"
? inputs
: outputs;
}
useEffect(() => {
setSelectedViewField(undefined);
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
}, [inputs.length, outputs.length]);
return (
<BaseModal
size={handleSelectChange() ? "large" : "small"}
size={haveChat ? "large" : "small"}
open={open}
setOpen={setOpen}
>
@ -85,125 +61,173 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
</BaseModal.Header>
<BaseModal.Content>
<div className="flex-max-width mt-2 h-[80vh]">
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide",
handleSelectChange() ? "w-2/6" : "w-full"
)}
>
<div className="flex w-full items-center justify-between py-2">
<div className="flex items-start gap-4">
{categories.map((category, index) => {
return (
//hide chat button if chat is alredy on the view
<Button
onClick={() => setSelectedCategory(index)}
variant={
index === selectedCategory ? "primary" : "secondary"
}
key={index}
>
<IconComponent
name={category.icon}
className=" file-component-variable"
/>
<span className="file-component-variables-span text-md">
{category.name}
</span>
</Button>
);
})}
</div>
{(outputs.map((output) => output.type).includes("ChatOutput") ||
inputs.map((output) => output.type).includes("chatInput")) &&
selectedView.type !== "ChatOutput" && (
<Button
onClick={() => setSelectedView({ type: "ChatOutput" })}
variant="outline"
key={"chat"}
className="self-end px-2.5"
>
<IconComponent
name="MessageSquareMore"
className="h-5 w-5"
/>
</Button>
)}
</div>
<div className="mx-2 mb-2 mt-4 flex items-center gap-2 font-semibold">
{categories[selectedCategory]?.name === "Inputs" && (
<>
<IconComponent name={"FormInput"} />
Text Inputs
</>
{selectedTab !== 0 && (
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start overflow-auto scrollbar-hide",
haveChat ? "w-2/6" : "w-full"
)}
{categories[selectedCategory]?.name === "Outputs" && (
<>
<IconComponent name={"ChevronRightSquare"} />
Prompt Outputs
</>
)}
</div>
{UpdateAccordion()
.filter(
(input) =>
input.type !== "ChatInput" && input.type !== "ChatOutput"
)
.map((input, index) => {
const node: NodeType = nodes.find(
(node) => node.id === input.id
)!;
return (
<div className="file-component-accordion-div" key={index}>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{input.id}
</Badge>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedView({
type: input.type,
id: input.id,
});
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
</div>
}
key={index}
keyValue={input.id}
>
<div className="file-component-tab-column">
<div className="">
{node &&
(categories[selectedCategory]?.name === "Inputs" ? (
<IOInputField
inputType={input.type}
inputId={input.id}
/>
) : (
<IOOutputView
outputType={input.type}
outputId={input.id}
/>
))}
</div>
</div>
</AccordionComponent>
>
<Tabs
value={selectedTab.toString()}
className={"api-modal-tabs "}
onValueChange={(value) => {
setSelectedTab(Number(value));
}}
>
<div className="api-modal-tablist-div">
<TabsList>
{inputs.length > 0 && (
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
)}
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent
value={"1"}
className="api-modal-tabs-content mt-4"
>
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"Type"} />
Text Inputs
</div>
);
})}
</div>
{handleSelectChange() ? (
handleSelectChange()
{nodes
.filter((node) =>
inputs.some((input) => input.id === node.id)
)
.map((node, index) => {
const input = inputs.find(
(input) => input.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{input.id}
</Badge>
{haveChat && (
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(input);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
)}
</div>
}
key={index}
keyValue={input.id}
>
<div className="file-component-tab-column">
<div className="">
{input && (
<IOInputField
inputType={input.type}
inputId={input.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent
value={"2"}
className="api-modal-tabs-content mt-4"
>
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"Braces"} />
Prompt Outputs
</div>
{nodes
.filter((node) =>
outputs.some((output) => output.id === node.id)
)
.map((node, index) => {
const output = outputs.find(
(output) => output.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{output.id}
</Badge>
{haveChat && (
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(output);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
)}
</div>
}
key={index}
keyValue={output.id}
>
<div className="file-component-tab-column">
<div className="">
{output && (
<IOOutputView
outputType={output.type}
outputId={output.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
</Tabs>
</div>
)}
{haveChat ? (
selectedViewField ? (
inputs.some((input) => input.id === selectedViewField.id) ? (
<IOInputField
inputType={selectedViewField.type!}
inputId={selectedViewField.id!}
/>
) : (
<IOOutputView
outputType={selectedViewField.type!}
outputId={selectedViewField.id!}
/>
)
) : (
<NewChatView />
)
) : (
<div className="absolute bottom-8 right-8">
<Button className="px-3">

View file

@ -14,7 +14,7 @@ import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { cn } from "../../../../utils/utils";
import Tooltip from "../../../TooltipComponent";
import ShadTooltip from "../../../ShadTooltipComponent";
import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button";
@ -125,8 +125,8 @@ export const MenuBar = ({
setOpen={setOpenSettings}
></FlowSettingsModal>
</div>
<Tooltip
title={
<ShadTooltip
content={
"Last saved at " +
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
hour: "numeric",
@ -134,8 +134,10 @@ export const MenuBar = ({
second: "numeric",
})
}
side="bottom"
styleClasses="cursor-default"
>
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground">
<IconComponent
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
@ -145,7 +147,7 @@ export const MenuBar = ({
/>
{printByBuildStatus()}
</div>
</Tooltip>
</ShadTooltip>
</div>
) : (
<></>

View file

@ -17,7 +17,7 @@ import { classNames } from "../../utils/utils";
import ChatInput from "./chatInput";
import ChatMessage from "./chatMessage";
export default function newChatView(): JSX.Element {
export default function NewChatView(): JSX.Element {
const [chatValue, setChatValue] = useState("");
const {
flowPool,

View file

@ -681,4 +681,4 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
export const priorityFields = new Set(["code", "template"]);
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
export const OUTPUT_TYPES = new Set(["ChatOutput"]);
export const OUTPUT_TYPES = new Set(["ChatOutput", "PromptTemplate"]);

View file

@ -182,7 +182,7 @@ export default function GenericModal({
{myModalTitle}
</span>
<IconComponent
name="FileText"
name={myModalTitle === "Edit Prompt" ? "TerminalSquare" : "FileText"}
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
@ -255,7 +255,7 @@ export default function GenericModal({
>
<div className="flex flex-wrap items-center">
<IconComponent
name="Variable"
name="Braces"
className=" -ml-px mr-1 flex h-4 w-4 text-primary"
/>
<span className="text-md font-semibold text-primary">

View file

@ -24,6 +24,7 @@ import {
generateNodeFromFlow,
getNodeId,
isValidConnection,
reconnectEdges,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
@ -394,7 +395,7 @@ export default function Page({
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
const { newFlow, removedEdges } = generateFlow(
lastSelection!,
nodes,
edges,
@ -404,6 +405,10 @@ export default function Page({
newFlow,
getNodeId
);
const newEdges = reconnectEdges(
newGroupNode,
removedEdges
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
@ -414,16 +419,17 @@ export default function Page({
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
setEdges((oldEdges) => [
...oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
),
...newEdges,
]);
} else {
setErrorData({
title: "Invalid selection",

View file

@ -25,7 +25,7 @@ import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
import { classNames, cn } from "../../../../utils/utils";
export default function NodeToolbarComponent({
data,
@ -60,6 +60,7 @@ export default function NodeToolbarComponent({
const isMinimal = numberOfHandles <= 1;
const isGroup = data.node?.flow ? true : false;
const pinned = data.node?.pinned ?? false;
const paste = useFlowStore((state) => state.paste);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -107,6 +108,9 @@ export default function NodeToolbarComponent({
takeSnapshot();
setShowNode(data.showNode ?? true ? false : true);
break;
case "Share":
if (hasApiKey || hasStore) setShowconfirmShare(true);
break;
case "Download":
downloadNode(flowComponent!);
break;
@ -222,7 +226,7 @@ export default function NodeToolbarComponent({
id={"code-input-node-toolbar-" + name}
/>
</div>
<IconComponent name="Code2" className="h-4 w-4" />
<IconComponent name="TerminalSquare" className="h-4 w-4" />
</button>
</ShadTooltip>
) : (
@ -269,22 +273,35 @@ export default function NodeToolbarComponent({
<IconComponent name="Copy" className="h-4 w-4" />
</button>
</ShadTooltip>
{hasStore && (
<ShadTooltip content="Share" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
!hasApiKey || !validApiKey ? " text-muted-foreground" : ""
<ShadTooltip content="Pin" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
)}
onClick={(event) => {
event.preventDefault();
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
pinned: old.data?.node?.pinned ? false : true,
},
},
}));
}}
>
<IconComponent
name="Pin"
className={cn(
"h-4 w-4 transition-all",
pinned ? "animate-wiggle fill-current" : ""
)}
onClick={(event) => {
event.preventDefault();
if (hasApiKey || hasStore) setShowconfirmShare(true);
}}
>
<IconComponent name="Share3" className="-m-1 h-6 w-6" />
</button>
</ShadTooltip>
)}
/>
</button>
</ShadTooltip>
<Select onValueChange={handleSelectChange} value="">
<ShadTooltip content="More" side="top">
@ -340,6 +357,20 @@ export default function NodeToolbarComponent({
</SelectItem>
)
)}
{hasStore && (
<SelectItem
value={"Share"}
disabled={!hasApiKey || !validApiKey}
>
<div className="flex" data-testid="save-button-modal">
<IconComponent
name="Share3"
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
/>{" "}
Share{" "}
</div>{" "}
</SelectItem>
)}
{!hasStore && (
<SelectItem value={"Download"}>
<div className="flex">

View file

@ -845,7 +845,7 @@
@apply flex h-full max-w-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
}
.api-modal-tablist-div {
@apply flex items-center justify-between px-2;
@apply flex items-center justify-between px-2 py-2;
}
.api-modal-tabs-content {
@apply -mt-1 h-full w-full overflow-hidden px-4 pb-4;

View file

@ -597,8 +597,7 @@ export function generateFlow(
const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } };
const uid = new ShortUniqueId({ length: 5 });
/* remove edges that are not connected to selected nodes on both ends
in future we can save this edges to when ungrouping reconect to the old nodes
*/
*/
newFlowData.edges = selection.edges.filter(
(edge) =>
selection.nodes.some((node) => node.id === edge.target) &&
@ -619,12 +618,48 @@ export function generateFlow(
// in the future we can use a better aproach using a set
return {
newFlow,
removedEdges: selection.edges.filter(
(edge) => !newFlowData.edges.includes(edge)
removedEdges: edges.filter(
(edge) =>
(selection.nodes.some((node) => node.id === edge.target) ||
selection.nodes.some((node) => node.id === edge.source)) &&
newFlowData.edges.every((e) => e.id !== edge.id)
),
};
}
export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
let newEdges = cloneDeep(excludedEdges);
if (!groupNode.data.node!.flow) return [];
const { nodes, edges } = groupNode.data.node!.flow!.data!;
const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
newEdges.forEach((edge) => {
if (lastNode && edge.source === lastNode.id) {
edge.source = groupNode.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
newSourceHandle.id = groupNode.id;
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
edge.data.sourceHandle = newSourceHandle;
}
if (nodes.some((node) => node.id === edge.target)) {
const targetNode = nodes.find((node) => node.id === edge.target)!;
console.log("targetNode", targetNode);
const targetHandle: targetHandleType = scapeJSONParse(edge.targetHandle!);
console.log("targetHandle", targetHandle);
const proxy = { id: targetNode.id, field: targetHandle.fieldName };
let newTargetHandle: targetHandleType = cloneDeep(targetHandle);
newTargetHandle.id = groupNode.id;
newTargetHandle.proxy = proxy;
edge.target = groupNode.id;
newTargetHandle.fieldName = targetHandle.fieldName + "_" + targetNode.id;
edge.targetHandle = scapedJSONStringfy(newTargetHandle);
edge.data.targetHandle = newTargetHandle;
}
});
return newEdges;
}
export function filterFlow(
selection: OnSelectionChangeParams,
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,

View file

@ -6,6 +6,7 @@ import {
BookmarkPlus,
Bot,
Boxes,
Braces,
Cable,
Check,
CheckCircle2,
@ -18,6 +19,7 @@ import {
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clipboard,
Code,
Code2,
@ -102,6 +104,7 @@ import {
TextCursorInput,
ToyBrick,
Trash2,
Type,
Undo,
Ungroup,
Unplug,
@ -345,6 +348,7 @@ export const nodeIconsLucide: iconsType = {
FileSearch2,
ChevronRight,
Circle,
CircleDot,
Clipboard,
Code2,
Variable,
@ -414,6 +418,8 @@ export const nodeIconsLucide: iconsType = {
Sliders,
ScreenShare,
Code,
Type,
Braces,
FlaskConical,
AlertCircle,
Bot,