New group node (#891)
This commit is contained in:
commit
99662ccc23
57 changed files with 4422 additions and 1215 deletions
|
|
@ -7,10 +7,11 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
import ZoomableImage from "/src/theme/ZoomableImage.js";
|
||||||
|
|
||||||
{" "}
|
{" "}
|
||||||
|
|
||||||
<ZoomableImage
|
<ZoomableImage
|
||||||
alt="Docusaurus themed image"
|
alt="Docusaurus themed image"
|
||||||
sources={{
|
sources={{
|
||||||
light: "img/new_langflow2.gif",
|
light: "img/new_langflow.gif",
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
BIN
docs/static/img/new_langflow.gif
vendored
BIN
docs/static/img/new_langflow.gif
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB |
BIN
docs/static/img/new_langflow2.gif
vendored
BIN
docs/static/img/new_langflow2.gif
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
BIN
docs/static/videos/langflow_fork.mp4
vendored
BIN
docs/static/videos/langflow_fork.mp4
vendored
Binary file not shown.
BIN
docs/static/videos/langflow_parameters.mp4
vendored
BIN
docs/static/videos/langflow_parameters.mp4
vendored
Binary file not shown.
BIN
docs/static/videos/langflow_widget.mp4
vendored
BIN
docs/static/videos/langflow_widget.mp4
vendored
Binary file not shown.
845
poetry.lock
generated
845
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -187,14 +187,18 @@ async def stream_build(
|
||||||
valid = False
|
valid = False
|
||||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||||
|
|
||||||
response = {
|
vertex_id = (
|
||||||
"valid": valid,
|
vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
|
||||||
"params": params,
|
)
|
||||||
"id": vertex.id,
|
if vertex_id in graph.top_level_nodes:
|
||||||
"progress": round(i / number_of_nodes, 2),
|
response = {
|
||||||
}
|
"valid": valid,
|
||||||
|
"params": params,
|
||||||
|
"id": vertex_id,
|
||||||
|
"progress": round(i / number_of_nodes, 2),
|
||||||
|
}
|
||||||
|
|
||||||
yield str(StreamData(event="message", data=response))
|
yield str(StreamData(event="message", data=response))
|
||||||
|
|
||||||
langchain_object = graph.build()
|
langchain_object = graph.build()
|
||||||
# Now we need to check the input_keys to send them to the client
|
# Now we need to check the input_keys to send them to the client
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ def get_all(
|
||||||
"/process/{flow_id}",
|
"/process/{flow_id}",
|
||||||
response_model=ProcessResponse,
|
response_model=ProcessResponse,
|
||||||
)
|
)
|
||||||
async def process_flow(
|
async def process(
|
||||||
session: Annotated[Session, Depends(get_session)],
|
session: Annotated[Session, Depends(get_session)],
|
||||||
flow_id: str,
|
flow_id: str,
|
||||||
inputs: Optional[dict] = None,
|
inputs: Optional[dict] = None,
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,79 @@
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.graph.vertex.base import Vertex
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
|
|
||||||
|
class SourceHandle(BaseModel):
|
||||||
|
baseClasses: List[str] = Field(
|
||||||
|
..., description="List of base classes for the source handle."
|
||||||
|
)
|
||||||
|
dataType: str = Field(..., description="Data type for the source handle.")
|
||||||
|
id: str = Field(..., description="Unique identifier for the source handle.")
|
||||||
|
|
||||||
|
|
||||||
|
class TargetHandle(BaseModel):
|
||||||
|
fieldName: str = Field(..., description="Field name for the target handle.")
|
||||||
|
id: str = Field(..., description="Unique identifier for the target handle.")
|
||||||
|
inputTypes: Optional[List[str]] = Field(
|
||||||
|
None, description="List of input types for the target handle."
|
||||||
|
)
|
||||||
|
type: str = Field(..., description="Type of the target handle.")
|
||||||
|
|
||||||
|
|
||||||
class Edge:
|
class Edge:
|
||||||
def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
|
def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
|
||||||
self.source: "Vertex" = source
|
self.source: "Vertex" = source
|
||||||
self.target: "Vertex" = target
|
self.target: "Vertex" = target
|
||||||
self.source_handle = edge.get("sourceHandle", "")
|
if data := edge.get("data", {}):
|
||||||
self.target_handle = edge.get("targetHandle", "")
|
self._source_handle = data.get("sourceHandle", {})
|
||||||
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
|
self._target_handle = data.get("targetHandle", {})
|
||||||
# target_param is documents
|
self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
|
||||||
self.target_param = self.target_handle.split("|")[1]
|
self.target_handle: TargetHandle = TargetHandle(**self._target_handle)
|
||||||
|
self.target_param = self.target_handle.fieldName
|
||||||
|
# validate handles
|
||||||
|
self.validate_handles()
|
||||||
|
else:
|
||||||
|
# Logging here because this is a breaking change
|
||||||
|
logger.error("Edge data is empty")
|
||||||
|
self._source_handle = edge.get("sourceHandle", "")
|
||||||
|
self._target_handle = edge.get("targetHandle", "")
|
||||||
|
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
|
||||||
|
# target_param is documents
|
||||||
|
self.target_param = self._target_handle.split("|")[1]
|
||||||
|
# Validate in __init__ to fail fast
|
||||||
self.validate_edge()
|
self.validate_edge()
|
||||||
|
|
||||||
|
def validate_handles(self) -> None:
|
||||||
|
if self.target_handle.inputTypes is None:
|
||||||
|
self.valid_handles = (
|
||||||
|
self.target_handle.type in self.source_handle.baseClasses
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.valid_handles = (
|
||||||
|
any(
|
||||||
|
baseClass in self.target_handle.inputTypes
|
||||||
|
for baseClass in self.source_handle.baseClasses
|
||||||
|
)
|
||||||
|
or self.target_handle.type in self.source_handle.baseClasses
|
||||||
|
)
|
||||||
|
if not self.valid_handles:
|
||||||
|
logger.debug(self.source_handle)
|
||||||
|
logger.debug(self.target_handle)
|
||||||
|
raise ValueError(
|
||||||
|
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
|
||||||
|
f"has invalid handles"
|
||||||
|
)
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
self.source = state["source"]
|
self.source = state["source"]
|
||||||
self.target = state["target"]
|
self.target = state["target"]
|
||||||
self.target_param = state["target_param"]
|
self.target_param = state["target_param"]
|
||||||
self.source_handle = state["source_handle"]
|
self.source_handle = state.get("source_handle")
|
||||||
self.target_handle = state["target_handle"]
|
self.target_handle = state.get("target_handle")
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
self.source._build_params()
|
self.source._build_params()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from typing import Dict, Generator, List, Type, Union
|
||||||
|
|
||||||
from langflow.graph.edge.base import Edge
|
from langflow.graph.edge.base import Edge
|
||||||
from langflow.graph.graph.constants import lazy_load_vertex_dict
|
from langflow.graph.graph.constants import lazy_load_vertex_dict
|
||||||
|
from langflow.graph.graph.utils import process_flow
|
||||||
from langflow.graph.vertex.base import Vertex
|
from langflow.graph.vertex.base import Vertex
|
||||||
from langflow.graph.vertex.types import (
|
from langflow.graph.vertex.types import (
|
||||||
FileToolVertex,
|
FileToolVertex,
|
||||||
|
|
@ -19,11 +20,21 @@ class Graph:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]],
|
nodes: List[Dict],
|
||||||
edges: List[Dict[str, str]],
|
edges: List[Dict[str, str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
self._nodes = nodes
|
self._nodes = nodes
|
||||||
self._edges = edges
|
self._edges = edges
|
||||||
|
self.raw_graph_data = {"nodes": nodes, "edges": edges}
|
||||||
|
|
||||||
|
self.top_level_nodes = []
|
||||||
|
for node in self._nodes:
|
||||||
|
if node_id := node.get("id"):
|
||||||
|
self.top_level_nodes.append(node_id)
|
||||||
|
|
||||||
|
self._graph_data = process_flow(self.raw_graph_data)
|
||||||
|
self._nodes = self._graph_data["nodes"]
|
||||||
|
self._edges = self._graph_data["edges"]
|
||||||
self._build_graph()
|
self._build_graph()
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
|
|
@ -50,6 +61,7 @@ class Graph:
|
||||||
edges = payload["edges"]
|
edges = payload["edges"]
|
||||||
return cls(nodes, edges)
|
return cls(nodes, edges)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
|
logger.exception(exc)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
|
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
@ -215,7 +227,9 @@ class Graph:
|
||||||
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
|
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
|
||||||
|
|
||||||
VertexClass = self._get_vertex_class(node_type, node_lc_type)
|
VertexClass = self._get_vertex_class(node_type, node_lc_type)
|
||||||
nodes.append(VertexClass(node))
|
vertex = VertexClass(node)
|
||||||
|
vertex.set_top_level(self.top_level_nodes)
|
||||||
|
nodes.append(vertex)
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
from collections import deque
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
def find_last_node(nodes, edges):
|
||||||
|
"""
|
||||||
|
This function receives a flow and returns the last node.
|
||||||
|
"""
|
||||||
|
return next((n for n in nodes if all(e["source"] != n["id"] for e in edges)), None)
|
||||||
|
|
||||||
|
|
||||||
|
def add_parent_node_id(nodes, parent_node_id):
|
||||||
|
"""
|
||||||
|
This function receives a list of nodes and adds a parent_node_id to each node.
|
||||||
|
"""
|
||||||
|
for node in nodes:
|
||||||
|
node["parent_node_id"] = parent_node_id
|
||||||
|
|
||||||
|
|
||||||
|
def ungroup_node(group_node_data, base_flow):
|
||||||
|
template, flow = (
|
||||||
|
group_node_data["node"]["template"],
|
||||||
|
group_node_data["node"]["flow"],
|
||||||
|
)
|
||||||
|
parent_node_id = group_node_data["id"]
|
||||||
|
g_nodes = flow["data"]["nodes"]
|
||||||
|
add_parent_node_id(g_nodes, parent_node_id)
|
||||||
|
g_edges = flow["data"]["edges"]
|
||||||
|
|
||||||
|
# Redirect edges to the correct proxy node
|
||||||
|
updated_edges = get_updated_edges(
|
||||||
|
base_flow, g_nodes, g_edges, group_node_data["id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update template values
|
||||||
|
update_template(template, g_nodes)
|
||||||
|
|
||||||
|
nodes = [
|
||||||
|
n for n in base_flow["nodes"] if n["id"] != group_node_data["id"]
|
||||||
|
] + g_nodes
|
||||||
|
edges = (
|
||||||
|
[
|
||||||
|
e
|
||||||
|
for e in base_flow["edges"]
|
||||||
|
if e["target"] != group_node_data["id"]
|
||||||
|
and e["source"] != group_node_data["id"]
|
||||||
|
]
|
||||||
|
+ g_edges
|
||||||
|
+ updated_edges
|
||||||
|
)
|
||||||
|
|
||||||
|
base_flow["nodes"] = nodes
|
||||||
|
base_flow["edges"] = edges
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def process_flow(flow_object):
|
||||||
|
cloned_flow = copy.deepcopy(flow_object)
|
||||||
|
processed_nodes = set() # To keep track of processed nodes
|
||||||
|
|
||||||
|
def process_node(node):
|
||||||
|
node_id = node.get("id")
|
||||||
|
|
||||||
|
# If node already processed, skip
|
||||||
|
if node_id in processed_nodes:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.get("data")
|
||||||
|
and node["data"].get("node")
|
||||||
|
and node["data"]["node"].get("flow")
|
||||||
|
):
|
||||||
|
process_flow(node["data"]["node"]["flow"]["data"])
|
||||||
|
new_nodes = ungroup_node(node["data"], cloned_flow)
|
||||||
|
# Add new nodes to the queue for future processing
|
||||||
|
nodes_to_process.extend(new_nodes)
|
||||||
|
|
||||||
|
# Mark node as processed
|
||||||
|
processed_nodes.add(node_id)
|
||||||
|
|
||||||
|
nodes_to_process = deque(cloned_flow["nodes"])
|
||||||
|
|
||||||
|
while nodes_to_process:
|
||||||
|
node = nodes_to_process.popleft()
|
||||||
|
process_node(node)
|
||||||
|
|
||||||
|
return cloned_flow
|
||||||
|
|
||||||
|
|
||||||
|
def update_template(template, g_nodes):
|
||||||
|
"""
|
||||||
|
Updates the template of a node in a graph with the given template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template (dict): The new template to update the node with.
|
||||||
|
g_nodes (list): The list of nodes in the graph.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
for _, value in template.items():
|
||||||
|
if not value.get("proxy"):
|
||||||
|
continue
|
||||||
|
proxy_dict = value["proxy"]
|
||||||
|
field, id_ = proxy_dict["field"], proxy_dict["id"]
|
||||||
|
node_index = next((i for i, n in enumerate(g_nodes) if n["id"] == id_), -1)
|
||||||
|
if node_index != -1:
|
||||||
|
display_name = None
|
||||||
|
show = g_nodes[node_index]["data"]["node"]["template"][field]["show"]
|
||||||
|
advanced = g_nodes[node_index]["data"]["node"]["template"][field][
|
||||||
|
"advanced"
|
||||||
|
]
|
||||||
|
if "display_name" in g_nodes[node_index]["data"]["node"]["template"][field]:
|
||||||
|
display_name = g_nodes[node_index]["data"]["node"]["template"][field][
|
||||||
|
"display_name"
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
display_name = g_nodes[node_index]["data"]["node"]["template"][field][
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
|
||||||
|
g_nodes[node_index]["data"]["node"]["template"][field] = value
|
||||||
|
g_nodes[node_index]["data"]["node"]["template"][field]["show"] = show
|
||||||
|
g_nodes[node_index]["data"]["node"]["template"][field][
|
||||||
|
"advanced"
|
||||||
|
] = advanced
|
||||||
|
g_nodes[node_index]["data"]["node"]["template"][field][
|
||||||
|
"display_name"
|
||||||
|
] = display_name
|
||||||
|
|
||||||
|
|
||||||
|
def update_target_handle(new_edge, g_nodes, group_node_id):
|
||||||
|
"""
|
||||||
|
Updates the target handle of a given edge if it is a proxy node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_edge (dict): The edge to update.
|
||||||
|
g_nodes (list): The list of nodes in the graph.
|
||||||
|
group_node_id (str): The ID of the group node.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The updated edge.
|
||||||
|
"""
|
||||||
|
target_handle = new_edge["data"]["targetHandle"]
|
||||||
|
if target_handle.get("proxy"):
|
||||||
|
proxy_id = target_handle["proxy"]["id"]
|
||||||
|
if node := next((n for n in g_nodes if n["id"] == proxy_id), None):
|
||||||
|
set_new_target_handle(proxy_id, new_edge, target_handle, node)
|
||||||
|
return new_edge
|
||||||
|
|
||||||
|
|
||||||
|
def set_new_target_handle(proxy_id, new_edge, target_handle, node):
|
||||||
|
"""
|
||||||
|
Sets a new target handle for a given edge.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
proxy_id (str): The ID of the proxy.
|
||||||
|
new_edge (dict): The new edge to be created.
|
||||||
|
target_handle (dict): The target handle of the edge.
|
||||||
|
node (dict): The node containing the edge.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
new_edge["target"] = proxy_id
|
||||||
|
_type = target_handle.get("type")
|
||||||
|
if _type is None:
|
||||||
|
raise KeyError("The 'type' key must be present in target_handle.")
|
||||||
|
|
||||||
|
field = target_handle["proxy"]["field"]
|
||||||
|
new_target_handle = {
|
||||||
|
"fieldName": field,
|
||||||
|
"type": _type,
|
||||||
|
"id": proxy_id,
|
||||||
|
}
|
||||||
|
if node["data"]["node"].get("flow"):
|
||||||
|
new_target_handle["proxy"] = {
|
||||||
|
"field": node["data"]["node"]["template"][field]["proxy"]["field"],
|
||||||
|
"id": node["data"]["node"]["template"][field]["proxy"]["id"],
|
||||||
|
}
|
||||||
|
if input_types := target_handle.get("inputTypes"):
|
||||||
|
new_target_handle["inputTypes"] = input_types
|
||||||
|
new_edge["data"]["targetHandle"] = new_target_handle
|
||||||
|
|
||||||
|
|
||||||
|
def update_source_handle(new_edge, g_nodes, g_edges):
|
||||||
|
"""
|
||||||
|
Updates the source handle of a given edge to the last node in the flow data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_edge (dict): The edge to update.
|
||||||
|
flow_data (dict): The flow data containing the nodes and edges.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The updated edge with the new source handle.
|
||||||
|
"""
|
||||||
|
last_node = copy.deepcopy(find_last_node(g_nodes, g_edges))
|
||||||
|
new_edge["source"] = last_node["id"]
|
||||||
|
new_source_handle = new_edge["data"]["sourceHandle"]
|
||||||
|
new_source_handle["id"] = last_node["id"]
|
||||||
|
new_edge["data"]["sourceHandle"] = new_source_handle
|
||||||
|
return new_edge
|
||||||
|
|
||||||
|
|
||||||
|
def get_updated_edges(base_flow, g_nodes, g_edges, group_node_id):
|
||||||
|
"""
|
||||||
|
Given a base flow, a list of graph nodes and a group node id, returns a list of updated edges.
|
||||||
|
An updated edge is an edge that has its target or source handle updated based on the group node id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_flow (dict): The base flow containing a list of edges.
|
||||||
|
g_nodes (list): A list of graph nodes.
|
||||||
|
group_node_id (str): The id of the group node.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of updated edges.
|
||||||
|
"""
|
||||||
|
updated_edges = []
|
||||||
|
for edge in base_flow["edges"]:
|
||||||
|
new_edge = copy.deepcopy(edge)
|
||||||
|
if new_edge["target"] == group_node_id:
|
||||||
|
new_edge = update_target_handle(new_edge, g_nodes, group_node_id)
|
||||||
|
|
||||||
|
if new_edge["source"] == group_node_id:
|
||||||
|
new_edge = update_source_handle(new_edge, g_nodes, g_edges)
|
||||||
|
|
||||||
|
if edge["target"] == group_node_id or edge["source"] == group_node_id:
|
||||||
|
updated_edges.append(new_edge)
|
||||||
|
return updated_edges
|
||||||
|
|
@ -38,6 +38,8 @@ class Vertex:
|
||||||
self.task_id: Optional[str] = None
|
self.task_id: Optional[str] = None
|
||||||
self.is_task = is_task
|
self.is_task = is_task
|
||||||
self.params = params or {}
|
self.params = params or {}
|
||||||
|
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
|
||||||
|
self.parent_is_top_level = False
|
||||||
|
|
||||||
def reset_params(self):
|
def reset_params(self):
|
||||||
for edge in self.edges:
|
for edge in self.edges:
|
||||||
|
|
@ -88,6 +90,11 @@ class Vertex:
|
||||||
self._built = False
|
self._built = False
|
||||||
self.artifacts: Dict[str, Any] = {}
|
self.artifacts: Dict[str, Any] = {}
|
||||||
self.task_id: Optional[str] = None
|
self.task_id: Optional[str] = None
|
||||||
|
self.parent_node_id = state["parent_node_id"]
|
||||||
|
self.parent_is_top_level = state["parent_is_top_level"]
|
||||||
|
|
||||||
|
def set_top_level(self, top_level_nodes: List[str]) -> None:
|
||||||
|
self.parent_is_top_level = self.parent_node_id in top_level_nodes
|
||||||
|
|
||||||
def _parse_data(self) -> None:
|
def _parse_data(self) -> None:
|
||||||
self.data = self._data["data"]
|
self.data = self._data["data"]
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class PromptFrontendNode(FrontendNode):
|
||||||
|
|
||||||
# All prompt fields should be password=False
|
# All prompt fields should be password=False
|
||||||
field.password = False
|
field.password = False
|
||||||
|
field.dynamic = True
|
||||||
|
|
||||||
|
|
||||||
class PromptTemplateNode(FrontendNode):
|
class PromptTemplateNode(FrontendNode):
|
||||||
|
|
|
||||||
2
src/frontend/.gitignore
vendored
2
src/frontend/.gitignore
vendored
|
|
@ -22,5 +22,5 @@ npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/*/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
|
|
||||||
648
src/frontend/package-lock.json
generated
648
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
src/frontend/playwright-report/index.html
Normal file
18
src/frontend/playwright-report/index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="./e2e/index.html">e2e report</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./onlyFront/index.html">frontEnd Only report</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -20,11 +20,13 @@ export default defineConfig({
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: "html",
|
reporter: [
|
||||||
|
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
|
||||||
|
],
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
// baseURL: 'http://127.0.0.1:3000',
|
// baseURL: "http://127.0.0.1:3000",
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
|
|
@ -69,9 +71,16 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
// webServer: {
|
// webServer: [
|
||||||
// command: 'npm run start',
|
// {
|
||||||
// url: 'http://127.0.0.1:3000',
|
// command: "npm run backend",
|
||||||
// reuseExistingServer: !process.env.CI,
|
// reuseExistingServer: !process.env.CI,
|
||||||
// },
|
// timeout: 120 * 1000,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// command: "npm run start",
|
||||||
|
// url: "http://127.0.0.1:3000",
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
75
src/frontend/run-tests.sh
Executable file
75
src/frontend/run-tests.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Default value for the --ui flag
|
||||||
|
ui=false
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
key="$1"
|
||||||
|
case $key in
|
||||||
|
--ui)
|
||||||
|
ui=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $key"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# Function to forcibly terminate a process by port
|
||||||
|
terminate_process_by_port() {
|
||||||
|
port="$1"
|
||||||
|
echo "Terminating process on port: $port"
|
||||||
|
fuser -k -n tcp "$port" # Forcefully terminate processes using the specified port
|
||||||
|
echo "Process terminated."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trap signals to ensure cleanup on script termination
|
||||||
|
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
|
||||||
|
|
||||||
|
# Navigate to the project root directory (where the Makefile is located)
|
||||||
|
cd ../../
|
||||||
|
|
||||||
|
# Start the frontend using 'make frontend' in the background
|
||||||
|
make frontend &
|
||||||
|
|
||||||
|
# Give some time for the frontend to start (adjust sleep duration as needed)
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Navigate to the test directory
|
||||||
|
cd src/frontend
|
||||||
|
|
||||||
|
# Run frontend only Playwright tests with or without UI based on the --ui flag
|
||||||
|
if [ "$ui" = true ]; then
|
||||||
|
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
|
||||||
|
else
|
||||||
|
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Navigate back to the project root directory
|
||||||
|
cd ../../
|
||||||
|
|
||||||
|
# Start the backend using 'make backend' in the background
|
||||||
|
make backend &
|
||||||
|
|
||||||
|
# Give some time for the backend to start (adjust sleep duration as needed)
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Navigate back to the test directory
|
||||||
|
cd src/frontend
|
||||||
|
|
||||||
|
# Run Playwright tests with or without UI based on the --ui flag
|
||||||
|
if [ "$ui" = true ]; then
|
||||||
|
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --ui --project=chromium
|
||||||
|
else
|
||||||
|
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --project=chromium
|
||||||
|
fi
|
||||||
|
|
||||||
|
npx playwright show-report
|
||||||
|
|
||||||
|
# After the tests are finished, you can add cleanup or teardown logic here if needed
|
||||||
|
|
||||||
|
# The trap will automatically terminate processes by port on script exit
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
convertValuesToNumbers,
|
convertValuesToNumbers,
|
||||||
hasDuplicateKeys,
|
hasDuplicateKeys,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
|
scapedJSONStringfy,
|
||||||
} from "../../../../utils/reactflowUtils";
|
} from "../../../../utils/reactflowUtils";
|
||||||
import {
|
import {
|
||||||
nodeColors,
|
nodeColors,
|
||||||
|
|
@ -53,6 +54,7 @@ export default function ParameterComponent({
|
||||||
required = false,
|
required = false,
|
||||||
optionalHandle = null,
|
optionalHandle = null,
|
||||||
info = "",
|
info = "",
|
||||||
|
proxy,
|
||||||
showNode,
|
showNode,
|
||||||
}: ParameterComponentType): JSX.Element {
|
}: ParameterComponentType): JSX.Element {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -80,8 +82,9 @@ export default function ParameterComponent({
|
||||||
|
|
||||||
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
|
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
|
||||||
let disabled =
|
let disabled =
|
||||||
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
|
reactFlowInstance
|
||||||
false;
|
?.getEdges()
|
||||||
|
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
|
||||||
|
|
||||||
const { data: myData } = useContext(typesContext);
|
const { data: myData } = useContext(typesContext);
|
||||||
|
|
||||||
|
|
@ -112,7 +115,6 @@ export default function ParameterComponent({
|
||||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (name === "openai_api_base") console.log(info);
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
infoHtml.current = (
|
infoHtml.current = (
|
||||||
<div className="h-full w-full break-words">
|
<div className="h-full w-full break-words">
|
||||||
|
|
@ -204,33 +206,46 @@ export default function ParameterComponent({
|
||||||
type === "code" ||
|
type === "code" ||
|
||||||
type === "prompt" ||
|
type === "prompt" ||
|
||||||
type === "file" ||
|
type === "file" ||
|
||||||
type === "int") &&
|
type === "int" ||
|
||||||
|
type === "dict" ||
|
||||||
|
type === "NestedDict") &&
|
||||||
!optionalHandle ? (
|
!optionalHandle ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<ShadTooltip
|
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
<div className="flex">
|
||||||
delayDuration={0}
|
<ShadTooltip
|
||||||
content={refHtml.current}
|
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||||
side={left ? "left" : "right"}
|
delayDuration={0}
|
||||||
>
|
content={refHtml.current}
|
||||||
<Handle
|
side={left ? "left" : "right"}
|
||||||
type={left ? "target" : "source"}
|
>
|
||||||
position={left ? Position.Left : Position.Right}
|
<Handle
|
||||||
id={id}
|
type={left ? "target" : "source"}
|
||||||
isValidConnection={(connection) =>
|
position={left ? Position.Left : Position.Right}
|
||||||
isValidConnection(connection, reactFlowInstance!)
|
id={
|
||||||
}
|
proxy
|
||||||
className={classNames(
|
? scapedJSONStringfy({ ...id, proxy })
|
||||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
: scapedJSONStringfy(id)
|
||||||
"h-3 w-3 rounded-full border-2 bg-background"
|
}
|
||||||
)}
|
isValidConnection={(connection) =>
|
||||||
style={{
|
isValidConnection(connection, reactFlowInstance!)
|
||||||
borderColor: color,
|
}
|
||||||
top: position,
|
className={classNames(
|
||||||
}}
|
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||||
></Handle>
|
"h-3 w-3 rounded-full border-2 bg-background"
|
||||||
</ShadTooltip>
|
)}
|
||||||
|
style={{
|
||||||
|
borderColor: color,
|
||||||
|
top: position,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterEdge(groupedEdge.current);
|
||||||
|
}}
|
||||||
|
></Handle>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
|
@ -245,7 +260,13 @@ export default function ParameterComponent({
|
||||||
(info !== "" ? " flex items-center" : "")
|
(info !== "" ? " flex items-center" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{title}
|
{proxy ? (
|
||||||
|
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||||
|
<span>{title}</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||||
<div className="">
|
<div className="">
|
||||||
{info !== "" && (
|
{info !== "" && (
|
||||||
|
|
@ -285,7 +306,11 @@ export default function ParameterComponent({
|
||||||
<Handle
|
<Handle
|
||||||
type={left ? "target" : "source"}
|
type={left ? "target" : "source"}
|
||||||
position={left ? Position.Left : Position.Right}
|
position={left ? Position.Left : Position.Right}
|
||||||
id={id}
|
id={
|
||||||
|
proxy
|
||||||
|
? scapedJSONStringfy({ ...id, proxy })
|
||||||
|
: scapedJSONStringfy(id)
|
||||||
|
}
|
||||||
isValidConnection={(connection) =>
|
isValidConnection={(connection) =>
|
||||||
isValidConnection(connection, reactFlowInstance!)
|
isValidConnection(connection, reactFlowInstance!)
|
||||||
}
|
}
|
||||||
|
|
@ -368,6 +393,11 @@ export default function ParameterComponent({
|
||||||
) : left === true && type === "code" ? (
|
) : left === true && type === "code" ? (
|
||||||
<div className="mt-2 w-full">
|
<div className="mt-2 w-full">
|
||||||
<CodeAreaComponent
|
<CodeAreaComponent
|
||||||
|
readonly={
|
||||||
|
data.node?.flow && data.node.template[name].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
dynamic={data.node?.template[name].dynamic ?? false}
|
dynamic={data.node?.template[name].dynamic ?? false}
|
||||||
setNodeClass={(nodeClass) => {
|
setNodeClass={(nodeClass) => {
|
||||||
data.node = nodeClass;
|
data.node = nodeClass;
|
||||||
|
|
@ -402,6 +432,11 @@ export default function ParameterComponent({
|
||||||
) : left === true && type === "prompt" ? (
|
) : left === true && type === "prompt" ? (
|
||||||
<div className="mt-2 w-full">
|
<div className="mt-2 w-full">
|
||||||
<PromptAreaComponent
|
<PromptAreaComponent
|
||||||
|
readonly={
|
||||||
|
data.node?.flow && data.node.template[name].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
field_name={name}
|
field_name={name}
|
||||||
setNodeClass={(nodeClass) => {
|
setNodeClass={(nodeClass) => {
|
||||||
data.node = nodeClass;
|
data.node = nodeClass;
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,33 @@ import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||||
import Tooltip from "../../components/TooltipComponent";
|
import Tooltip from "../../components/TooltipComponent";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
import InputComponent from "../../components/inputComponent";
|
||||||
|
import { Textarea } from "../../components/ui/textarea";
|
||||||
import { useSSE } from "../../contexts/SSEContext";
|
import { useSSE } from "../../contexts/SSEContext";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
import { typesContext } from "../../contexts/typesContext";
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||||
import { validationStatusType } from "../../types/components";
|
import { validationStatusType } from "../../types/components";
|
||||||
import { NodeDataType } from "../../types/flow";
|
import { NodeDataType } from "../../types/flow";
|
||||||
import { cleanEdges } from "../../utils/reactflowUtils";
|
import {
|
||||||
|
cleanEdges,
|
||||||
|
handleKeyDown,
|
||||||
|
scapedJSONStringfy,
|
||||||
|
} from "../../utils/reactflowUtils";
|
||||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||||
import { classNames, toTitleCase } from "../../utils/utils";
|
import { classNames, toTitleCase } from "../../utils/utils";
|
||||||
import ParameterComponent from "./components/parameterComponent";
|
import ParameterComponent from "./components/parameterComponent";
|
||||||
|
|
||||||
export default function GenericNode({
|
export default function GenericNode({
|
||||||
data: olddata,
|
data: olddata,
|
||||||
|
xPos,
|
||||||
|
yPos,
|
||||||
selected,
|
selected,
|
||||||
}: {
|
}: {
|
||||||
data: NodeDataType;
|
data: NodeDataType;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [data, setData] = useState(olddata);
|
const [data, setData] = useState(olddata);
|
||||||
const { updateFlow, flows, tabId } = useContext(TabsContext);
|
const { updateFlow, flows, tabId } = useContext(TabsContext);
|
||||||
|
|
@ -28,6 +38,12 @@ export default function GenericNode({
|
||||||
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
|
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
|
||||||
useContext(typesContext);
|
useContext(typesContext);
|
||||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||||
|
const [inputName, setInputName] = useState(true);
|
||||||
|
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||||
|
const [inputDescription, setInputDescription] = useState(false);
|
||||||
|
const [nodeDescription, setNodeDescription] = useState(
|
||||||
|
data.node?.description!
|
||||||
|
);
|
||||||
const [validationStatus, setValidationStatus] =
|
const [validationStatus, setValidationStatus] =
|
||||||
useState<validationStatusType | null>(null);
|
useState<validationStatusType | null>(null);
|
||||||
const [showNode, setShowNode] = useState<boolean>(true);
|
const [showNode, setShowNode] = useState<boolean>(true);
|
||||||
|
|
@ -111,6 +127,7 @@ export default function GenericNode({
|
||||||
<>
|
<>
|
||||||
<NodeToolbar>
|
<NodeToolbar>
|
||||||
<NodeToolbarComponent
|
<NodeToolbarComponent
|
||||||
|
position={{ x: xPos, y: yPos }}
|
||||||
data={data}
|
data={data}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
|
|
@ -151,7 +168,7 @@ export default function GenericNode({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name={name}
|
name={data.node?.flow ? "Ungroup" : name}
|
||||||
className={
|
className={
|
||||||
"generic-node-icon " +
|
"generic-node-icon " +
|
||||||
(!showNode && "absolute inset-x-6 h-12 w-12")
|
(!showNode && "absolute inset-x-6 h-12 w-12")
|
||||||
|
|
@ -160,11 +177,35 @@ export default function GenericNode({
|
||||||
/>
|
/>
|
||||||
{showNode && (
|
{showNode && (
|
||||||
<div className="generic-node-tooltip-div">
|
<div className="generic-node-tooltip-div">
|
||||||
<ShadTooltip content={data.node?.display_name}>
|
{data.node?.flow && inputName ? (
|
||||||
<div className="generic-node-tooltip-div text-primary">
|
<div>
|
||||||
{data.node?.display_name}
|
<InputComponent
|
||||||
|
autoFocus
|
||||||
|
onBlur={() => {
|
||||||
|
setInputName(false);
|
||||||
|
if (nodeName.trim() !== "") {
|
||||||
|
setNodeName(nodeName);
|
||||||
|
data.node!.display_name = nodeName;
|
||||||
|
} else {
|
||||||
|
setNodeName(data.node!.display_name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={nodeName}
|
||||||
|
onChange={setNodeName}
|
||||||
|
password={false}
|
||||||
|
blurOnEnter={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
) : (
|
||||||
|
<ShadTooltip content={data.node?.display_name}>
|
||||||
|
<div
|
||||||
|
className="generic-node-tooltip-div text-primary"
|
||||||
|
onDoubleClick={() => setInputName(true)}
|
||||||
|
>
|
||||||
|
{data.node?.display_name}
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -178,16 +219,14 @@ export default function GenericNode({
|
||||||
data.node!.template[templateField].show &&
|
data.node!.template[templateField].show &&
|
||||||
!data.node!.template[templateField].advanced && (
|
!data.node!.template[templateField].advanced && (
|
||||||
<ParameterComponent
|
<ParameterComponent
|
||||||
key={
|
key={scapedJSONStringfy({
|
||||||
(data.node!.template[
|
inputTypes:
|
||||||
templateField
|
data.node!.template[templateField].input_types,
|
||||||
].input_types?.join(";") ??
|
type: data.node!.template[templateField].type,
|
||||||
data.node!.template[templateField].type) +
|
id: data.id,
|
||||||
"|" +
|
fieldName: templateField,
|
||||||
templateField +
|
proxy: data.node!.template[templateField].proxy,
|
||||||
"|" +
|
})}
|
||||||
data.id
|
|
||||||
}
|
|
||||||
data={data}
|
data={data}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
color={
|
color={
|
||||||
|
|
@ -217,31 +256,31 @@ export default function GenericNode({
|
||||||
data.node?.template[templateField].type
|
data.node?.template[templateField].type
|
||||||
}
|
}
|
||||||
required={
|
required={
|
||||||
data.node?.template[templateField].required
|
data.node!.template[templateField].required
|
||||||
}
|
|
||||||
id={
|
|
||||||
(data.node?.template[
|
|
||||||
templateField
|
|
||||||
].input_types?.join(";") ??
|
|
||||||
data.node?.template[templateField].type) +
|
|
||||||
"|" +
|
|
||||||
templateField +
|
|
||||||
"|" +
|
|
||||||
data.id
|
|
||||||
}
|
}
|
||||||
|
id={{
|
||||||
|
inputTypes:
|
||||||
|
data.node!.template[templateField].input_types,
|
||||||
|
type: data.node!.template[templateField].type,
|
||||||
|
id: data.id,
|
||||||
|
fieldName: templateField,
|
||||||
|
}}
|
||||||
left={true}
|
left={true}
|
||||||
type={data.node?.template[templateField].type}
|
type={data.node?.template[templateField].type}
|
||||||
optionalHandle={
|
optionalHandle={
|
||||||
data.node?.template[templateField].input_types
|
data.node?.template[templateField].input_types
|
||||||
}
|
}
|
||||||
|
proxy={data.node?.template[templateField].proxy}
|
||||||
showNode={showNode}
|
showNode={showNode}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<ParameterComponent
|
<ParameterComponent
|
||||||
key={[data.type, data.id, ...data.node!.base_classes].join(
|
key={scapedJSONStringfy({
|
||||||
"|"
|
baseClasses: data.node!.base_classes,
|
||||||
)}
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
})}
|
||||||
data={data}
|
data={data}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||||
|
|
@ -252,9 +291,11 @@ export default function GenericNode({
|
||||||
: data.type
|
: data.type
|
||||||
}
|
}
|
||||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||||
id={[data.type, data.id, ...data.node!.base_classes].join(
|
id={{
|
||||||
"|"
|
baseClasses: data.node!.base_classes,
|
||||||
)}
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
}}
|
||||||
type={data.node?.base_classes.join("|")}
|
type={data.node?.base_classes.join("|")}
|
||||||
left={false}
|
left={false}
|
||||||
showNode={showNode}
|
showNode={showNode}
|
||||||
|
|
@ -330,16 +371,53 @@ export default function GenericNode({
|
||||||
className={
|
className={
|
||||||
showNode
|
showNode
|
||||||
? "generic-node-desc " +
|
? "generic-node-desc " +
|
||||||
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
|
(data.node?.description !== "" ? "py-5" : "pb-5")
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{data.node?.description !== "" && showNode && (
|
{data.node?.description !== "" &&
|
||||||
<div className="generic-node-desc-text">
|
showNode &&
|
||||||
|
data.node?.flow &&
|
||||||
|
inputDescription ? (
|
||||||
|
<Textarea
|
||||||
|
autoFocus
|
||||||
|
onBlur={() => {
|
||||||
|
setInputDescription(false);
|
||||||
|
if (nodeDescription.trim() !== "") {
|
||||||
|
setNodeDescription(nodeDescription);
|
||||||
|
data.node!.description = nodeDescription;
|
||||||
|
} else {
|
||||||
|
setNodeDescription(data.node!.description);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={nodeDescription}
|
||||||
|
onChange={(e) => setNodeDescription(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
handleKeyDown(e, nodeDescription, "");
|
||||||
|
if (
|
||||||
|
e.key === "Enter" &&
|
||||||
|
e.shiftKey === false &&
|
||||||
|
e.ctrlKey === false &&
|
||||||
|
e.altKey === false
|
||||||
|
) {
|
||||||
|
setInputDescription(false);
|
||||||
|
if (nodeDescription.trim() !== "") {
|
||||||
|
setNodeDescription(nodeDescription);
|
||||||
|
data.node!.description = nodeDescription;
|
||||||
|
} else {
|
||||||
|
setNodeDescription(data.node!.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="generic-node-desc-text"
|
||||||
|
onDoubleClick={() => setInputDescription(true)}
|
||||||
|
>
|
||||||
{data.node?.description}
|
{data.node?.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<>
|
<>
|
||||||
{Object.keys(data.node!.template)
|
{Object.keys(data.node!.template)
|
||||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||||
|
|
@ -348,15 +426,14 @@ export default function GenericNode({
|
||||||
{data.node!.template[templateField].show &&
|
{data.node!.template[templateField].show &&
|
||||||
!data.node!.template[templateField].advanced ? (
|
!data.node!.template[templateField].advanced ? (
|
||||||
<ParameterComponent
|
<ParameterComponent
|
||||||
key={
|
key={scapedJSONStringfy({
|
||||||
(data.node!.template[templateField].input_types?.join(
|
inputTypes:
|
||||||
";"
|
data.node!.template[templateField].input_types,
|
||||||
) ?? data.node!.template[templateField].type) +
|
type: data.node!.template[templateField].type,
|
||||||
"|" +
|
id: data.id,
|
||||||
templateField +
|
fieldName: templateField,
|
||||||
"|" +
|
proxy: data.node!.template[templateField].proxy,
|
||||||
data.id
|
})}
|
||||||
}
|
|
||||||
data={data}
|
data={data}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
color={
|
color={
|
||||||
|
|
@ -384,21 +461,20 @@ export default function GenericNode({
|
||||||
"\n"
|
"\n"
|
||||||
) ?? data.node?.template[templateField].type
|
) ?? data.node?.template[templateField].type
|
||||||
}
|
}
|
||||||
required={data.node?.template[templateField].required}
|
required={data.node!.template[templateField].required}
|
||||||
id={
|
id={{
|
||||||
(data.node?.template[templateField].input_types?.join(
|
inputTypes:
|
||||||
";"
|
data.node!.template[templateField].input_types,
|
||||||
) ?? data.node?.template[templateField].type) +
|
type: data.node!.template[templateField].type,
|
||||||
"|" +
|
id: data.id,
|
||||||
templateField +
|
fieldName: templateField,
|
||||||
"|" +
|
}}
|
||||||
data.id
|
|
||||||
}
|
|
||||||
left={true}
|
left={true}
|
||||||
type={data.node?.template[templateField].type}
|
type={data.node?.template[templateField].type}
|
||||||
optionalHandle={
|
optionalHandle={
|
||||||
data.node?.template[templateField].input_types
|
data.node?.template[templateField].input_types
|
||||||
}
|
}
|
||||||
|
proxy={data.node?.template[templateField].proxy}
|
||||||
showNode={showNode}
|
showNode={showNode}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -415,7 +491,11 @@ export default function GenericNode({
|
||||||
{" "}
|
{" "}
|
||||||
</div>
|
</div>
|
||||||
<ParameterComponent
|
<ParameterComponent
|
||||||
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
key={scapedJSONStringfy({
|
||||||
|
baseClasses: data.node!.base_classes,
|
||||||
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
})}
|
||||||
data={data}
|
data={data}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||||
|
|
@ -425,7 +505,11 @@ export default function GenericNode({
|
||||||
: data.type
|
: data.type
|
||||||
}
|
}
|
||||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||||
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
id={{
|
||||||
|
baseClasses: data.node!.base_classes,
|
||||||
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
}}
|
||||||
type={data.node?.base_classes.join("|")}
|
type={data.node?.base_classes.join("|")}
|
||||||
left={false}
|
left={false}
|
||||||
showNode={showNode}
|
showNode={showNode}
|
||||||
|
|
|
||||||
|
|
@ -80,61 +80,54 @@ export default function BuildTrigger({
|
||||||
const { flowId } = response.data;
|
const { flowId } = response.data;
|
||||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||||
let validationResults: boolean[] = [];
|
let validationResults: boolean[] = [];
|
||||||
let finished = false;
|
|
||||||
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||||
const eventSource = new EventSource(apiUrl);
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
|
const eventSource = new EventSource(apiUrl);
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
// If the event is parseable, return
|
// If the event is parseable, return
|
||||||
if (!event.data) {
|
if (!event.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parsedData = JSON.parse(event.data);
|
const parsedData = JSON.parse(event.data);
|
||||||
// if the event is the end of the stream, close the connection
|
// if the event is the end of the stream, close the connection
|
||||||
if (parsedData.end_of_stream) {
|
if (parsedData.end_of_stream) {
|
||||||
// Close the connection and finish
|
eventSource.close();
|
||||||
finished = true;
|
resolve(validationResults.every((result) => result));
|
||||||
|
} else if (parsedData.log) {
|
||||||
|
// If the event is a log, log it
|
||||||
|
setSuccessData({ title: parsedData.log });
|
||||||
|
} else if (parsedData.input_keys !== undefined) {
|
||||||
|
//@ts-ignore
|
||||||
|
setTabsState((old: TabsState) => {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
[flowId]: {
|
||||||
|
...old[flowId],
|
||||||
|
formKeysData: parsedData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Otherwise, process the data
|
||||||
|
const isValid = processStreamResult(parsedData);
|
||||||
|
setProgress(parsedData.progress);
|
||||||
|
validationResults.push(isValid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error: any) => {
|
||||||
|
console.error("EventSource failed:", error);
|
||||||
|
|
||||||
|
if (error.data) {
|
||||||
|
const parsedData = JSON.parse(error.data);
|
||||||
|
setErrorData({ title: parsedData.error });
|
||||||
|
setIsBuilding(false);
|
||||||
|
}
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
|
reject(new Error("Streaming failed"));
|
||||||
return;
|
};
|
||||||
} else if (parsedData.log) {
|
});
|
||||||
// If the event is a log, log it
|
|
||||||
setSuccessData({ title: parsedData.log });
|
|
||||||
} else if (parsedData.input_keys !== undefined) {
|
|
||||||
//@ts-ignore
|
|
||||||
setTabsState((old: TabsState) => {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
[flowId]: {
|
|
||||||
...old[flowId],
|
|
||||||
formKeysData: parsedData,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Otherwise, process the data
|
|
||||||
const isValid = processStreamResult(parsedData);
|
|
||||||
setProgress(parsedData.progress);
|
|
||||||
validationResults.push(isValid);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onerror = (error: any) => {
|
|
||||||
console.error("EventSource failed:", error);
|
|
||||||
eventSource.close();
|
|
||||||
if (error.data) {
|
|
||||||
const parsedData = JSON.parse(error.data);
|
|
||||||
setErrorData({ title: parsedData.error });
|
|
||||||
setIsBuilding(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Step 3: Wait for the stream to finish
|
|
||||||
while (!finished) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
finished = validationResults.length === flow.data!.nodes.length;
|
|
||||||
}
|
|
||||||
// Step 4: Return true if all nodes are valid, false otherwise
|
|
||||||
return validationResults.every((result) => result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processStreamResult(parsedData: parsedDataType) {
|
function processStreamResult(parsedData: parsedDataType) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default function CodeAreaComponent({
|
||||||
nodeClass,
|
nodeClass,
|
||||||
dynamic,
|
dynamic,
|
||||||
setNodeClass,
|
setNodeClass,
|
||||||
|
readonly = false,
|
||||||
}: CodeAreaComponentType) {
|
}: CodeAreaComponentType) {
|
||||||
const [myValue, setMyValue] = useState(
|
const [myValue, setMyValue] = useState(
|
||||||
typeof value == "string" ? value : JSON.stringify(value)
|
typeof value == "string" ? value : JSON.stringify(value)
|
||||||
|
|
@ -30,6 +31,7 @@ export default function CodeAreaComponent({
|
||||||
return (
|
return (
|
||||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||||
<CodeAreaModal
|
<CodeAreaModal
|
||||||
|
readonly={readonly}
|
||||||
dynamic={dynamic}
|
dynamic={dynamic}
|
||||||
value={myValue}
|
value={myValue}
|
||||||
nodeClass={nodeClass}
|
nodeClass={nodeClass}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export default function CodeTabsComponent({
|
||||||
}, [flow]);
|
}, [flow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tweaks) {
|
if (tweaks && data) {
|
||||||
unselectAllNodes({
|
unselectAllNodes({
|
||||||
data,
|
data,
|
||||||
updateNodes: (nodes) => {
|
updateNodes: (nodes) => {
|
||||||
|
|
@ -604,6 +604,14 @@ export default function CodeTabsComponent({
|
||||||
].type === "prompt" ? (
|
].type === "prompt" ? (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<PromptAreaComponent
|
<PromptAreaComponent
|
||||||
|
readonly={
|
||||||
|
node.data.node?.flow &&
|
||||||
|
node.data.node.template[
|
||||||
|
templateField
|
||||||
|
].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
value={
|
value={
|
||||||
|
|
@ -646,6 +654,14 @@ export default function CodeTabsComponent({
|
||||||
<CodeAreaComponent
|
<CodeAreaComponent
|
||||||
disabled={false}
|
disabled={false}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
|
readonly={
|
||||||
|
node.data.node?.flow &&
|
||||||
|
node.data.node.template[
|
||||||
|
templateField
|
||||||
|
].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
value={
|
value={
|
||||||
!node.data.node.template[
|
!node.data.node.template[
|
||||||
templateField
|
templateField
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
||||||
|
|
||||||
function handleAddFlow() {
|
function handleAddFlow() {
|
||||||
try {
|
try {
|
||||||
addFlow(undefined, true).then((id) => {
|
addFlow(true).then((id) => {
|
||||||
navigate("/flow/" + id);
|
navigate("/flow/" + id);
|
||||||
});
|
});
|
||||||
// saveFlowStyleInDataBase();
|
// saveFlowStyleInDataBase();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import * as Form from "@radix-ui/react-form";
|
import * as Form from "@radix-ui/react-form";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { InputComponentType } from "../../types/components";
|
import { InputComponentType } from "../../types/components";
|
||||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||||
import { classNames } from "../../utils/utils";
|
import { classNames } from "../../utils/utils";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
export default function InputComponent({
|
export default function InputComponent({
|
||||||
|
autoFocus = false,
|
||||||
|
onBlur,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
|
@ -15,9 +17,10 @@ export default function InputComponent({
|
||||||
editNode = false,
|
editNode = false,
|
||||||
placeholder = "Type something...",
|
placeholder = "Type something...",
|
||||||
className,
|
className,
|
||||||
|
blurOnEnter = false,
|
||||||
}: InputComponentType): JSX.Element {
|
}: InputComponentType): JSX.Element {
|
||||||
const [pwdVisible, setPwdVisible] = useState(false);
|
const [pwdVisible, setPwdVisible] = useState(false);
|
||||||
|
const refInput = useRef<HTMLInputElement>(null);
|
||||||
// Clear component state
|
// Clear component state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
|
|
@ -30,6 +33,9 @@ export default function InputComponent({
|
||||||
{isForm ? (
|
{isForm ? (
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input
|
<Input
|
||||||
|
ref={refInput}
|
||||||
|
onBlur={onBlur}
|
||||||
|
autoFocus={autoFocus}
|
||||||
type={password && !pwdVisible ? "password" : "text"}
|
type={password && !pwdVisible ? "password" : "text"}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -49,13 +55,17 @@ export default function InputComponent({
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
handleKeyDown(e, value, "");
|
handleKeyDown(e, value, "");
|
||||||
|
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
|
ref={refInput}
|
||||||
type="text"
|
type="text"
|
||||||
|
onBlur={onBlur}
|
||||||
value={value}
|
value={value}
|
||||||
|
autoFocus={autoFocus}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
@ -73,6 +83,7 @@ export default function InputComponent({
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
handleKeyDown(e, value, "");
|
handleKeyDown(e, value, "");
|
||||||
|
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ export default function PromptAreaComponent({
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
editNode = false,
|
editNode = false,
|
||||||
}: PromptAreaComponentType) {
|
readonly = false,
|
||||||
|
}: PromptAreaComponentType): JSX.Element {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
onChange("");
|
onChange("");
|
||||||
|
|
@ -22,7 +23,7 @@ export default function PromptAreaComponent({
|
||||||
}, [disabled]);
|
}, [disabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value !== "" && !editNode) {
|
if (value !== "" && !editNode && !readonly) {
|
||||||
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
|
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
|
||||||
if (apiReturn.data) {
|
if (apiReturn.data) {
|
||||||
setNodeClass!(apiReturn.data.frontend_node);
|
setNodeClass!(apiReturn.data.frontend_node);
|
||||||
|
|
@ -35,6 +36,7 @@ export default function PromptAreaComponent({
|
||||||
return (
|
return (
|
||||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||||
<GenericModal
|
<GenericModal
|
||||||
|
readonly={readonly}
|
||||||
type={TypeModal.PROMPT}
|
type={TypeModal.PROMPT}
|
||||||
value={value}
|
value={value}
|
||||||
buttonText="Check & Save"
|
buttonText="Check & Save"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
"z-50 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,20 @@ import {
|
||||||
} from "../controllers/API";
|
} from "../controllers/API";
|
||||||
import { APIClassType, APITemplateType } from "../types/api";
|
import { APIClassType, APITemplateType } from "../types/api";
|
||||||
import { tweakType } from "../types/components";
|
import { tweakType } from "../types/components";
|
||||||
import { FlowType, NodeDataType, NodeType } from "../types/flow";
|
import {
|
||||||
|
FlowType,
|
||||||
|
NodeDataType,
|
||||||
|
NodeType,
|
||||||
|
sourceHandleType,
|
||||||
|
targetHandleType,
|
||||||
|
} from "../types/flow";
|
||||||
import { TabsContextType, TabsState } from "../types/tabs";
|
import { TabsContextType, TabsState } from "../types/tabs";
|
||||||
import {
|
import {
|
||||||
addVersionToDuplicates,
|
addVersionToDuplicates,
|
||||||
|
checkOldEdgesHandles,
|
||||||
|
scapeJSONParse,
|
||||||
|
scapedJSONStringfy,
|
||||||
|
updateEdgesHandleIds,
|
||||||
updateIds,
|
updateIds,
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
} from "../utils/reactflowUtils";
|
} from "../utils/reactflowUtils";
|
||||||
|
|
@ -41,7 +51,7 @@ const TabsContextInitialValue: TabsContextType = {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
flows: [],
|
flows: [],
|
||||||
removeFlow: (id: string) => {},
|
removeFlow: (id: string) => {},
|
||||||
addFlow: async (flowData?: any) => "",
|
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
|
||||||
updateFlow: (newFlow: FlowType) => {},
|
updateFlow: (newFlow: FlowType) => {},
|
||||||
incrementNodeId: () => uid(),
|
incrementNodeId: () => uid(),
|
||||||
downloadFlow: (flow: FlowType) => {},
|
downloadFlow: (flow: FlowType) => {},
|
||||||
|
|
@ -134,14 +144,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
if (!flow.data) {
|
if (!flow.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
processFlowEdges(flow);
|
processDataFromFlow(flow, false);
|
||||||
processFlowNodes(flow);
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function processFlowEdges(flow: FlowType) {
|
function processFlowEdges(flow: FlowType) {
|
||||||
if (!flow.data || !flow.data.edges) return;
|
if (!flow.data || !flow.data.edges) return;
|
||||||
|
if (checkOldEdgesHandles(flow.data.edges)) {
|
||||||
|
const newEdges = updateEdgesHandleIds(flow.data);
|
||||||
|
flow.data.edges = newEdges;
|
||||||
|
}
|
||||||
|
//update edges colors
|
||||||
flow.data.edges.forEach((edge) => {
|
flow.data.edges.forEach((edge) => {
|
||||||
edge.className = "";
|
edge.className = "";
|
||||||
edge.style = { stroke: "#555" };
|
edge.style = { stroke: "#555" };
|
||||||
|
|
@ -159,6 +173,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
function processFlowNodes(flow: FlowType) {
|
function processFlowNodes(flow: FlowType) {
|
||||||
if (!flow.data || !flow.data.nodes) return;
|
if (!flow.data || !flow.data.nodes) return;
|
||||||
flow.data.nodes.forEach((node: NodeType) => {
|
flow.data.nodes.forEach((node: NodeType) => {
|
||||||
|
if (node.data.node?.flow) return;
|
||||||
if (skipNodeUpdate.includes(node.data.type)) return;
|
if (skipNodeUpdate.includes(node.data.type)) return;
|
||||||
const template = templates[node.data.type];
|
const template = templates[node.data.type];
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|
@ -168,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
if (Object.keys(template["template"]).length > 0) {
|
if (Object.keys(template["template"]).length > 0) {
|
||||||
updateDisplay_name(node, template);
|
updateDisplay_name(node, template);
|
||||||
updateNodeBaseClasses(node, template);
|
updateNodeBaseClasses(node, template);
|
||||||
|
//update baseclasses in edges
|
||||||
updateNodeEdges(flow, node, template);
|
updateNodeEdges(flow, node, template);
|
||||||
updateNodeDescription(node, template);
|
updateNodeDescription(node, template);
|
||||||
updateNodeTemplate(node, template);
|
updateNodeTemplate(node, template);
|
||||||
|
|
@ -187,11 +203,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
) {
|
) {
|
||||||
flow.data!.edges.forEach((edge) => {
|
flow.data!.edges.forEach((edge) => {
|
||||||
if (edge.source === node.id) {
|
if (edge.source === node.id) {
|
||||||
edge.sourceHandle = edge.sourceHandle
|
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||||
?.split("|")
|
edge.sourceHandle!
|
||||||
.slice(0, 2)
|
);
|
||||||
.concat(template["base_classes"])
|
sourceHandleObject.baseClasses = template["base_classes"];
|
||||||
.join("|");
|
edge.data.sourceHandle = sourceHandleObject;
|
||||||
|
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +291,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
* The resulting JSON object is passed to the addFlow function.
|
* The resulting JSON object is passed to the addFlow function.
|
||||||
*/
|
*/
|
||||||
async function uploadFlow(
|
async function uploadFlow(
|
||||||
newProject?: boolean,
|
newProject: boolean,
|
||||||
file?: File
|
file?: File
|
||||||
): Promise<String | undefined> {
|
): Promise<String | undefined> {
|
||||||
let id;
|
let id;
|
||||||
|
|
@ -283,7 +300,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
// parse the text into a JSON object
|
// parse the text into a JSON object
|
||||||
let flow: FlowType = JSON.parse(text);
|
let flow: FlowType = JSON.parse(text);
|
||||||
|
|
||||||
id = await addFlow(flow, newProject);
|
id = await addFlow(newProject, flow);
|
||||||
} else {
|
} else {
|
||||||
// create a file input
|
// create a file input
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
|
|
@ -298,7 +315,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
const currentfile = (e.target as HTMLInputElement).files![0];
|
const currentfile = (e.target as HTMLInputElement).files![0];
|
||||||
let text = await currentfile.text();
|
let text = await currentfile.text();
|
||||||
let flow: FlowType = JSON.parse(text);
|
let flow: FlowType = JSON.parse(text);
|
||||||
const flowId = await addFlow(flow, newProject);
|
const flowId = await addFlow(newProject, flow);
|
||||||
resolve(flowId);
|
resolve(flowId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -349,7 +366,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
* Add a new flow to the list of flows.
|
* Add a new flow to the list of flows.
|
||||||
* @param flow Optional flow to add.
|
* @param flow Optional flow to add.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function paste(
|
function paste(
|
||||||
selectionInstance: { nodes: Node[]; edges: Edge[] },
|
selectionInstance: { nodes: Node[]; edges: Edge[] },
|
||||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||||
|
|
@ -398,19 +414,27 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
reactFlowInstance!.setNodes(nodes);
|
reactFlowInstance!.setNodes(nodes);
|
||||||
|
|
||||||
selectionInstance.edges.forEach((edge) => {
|
selectionInstance.edges.forEach((edge: Edge) => {
|
||||||
let source = idsMap[edge.source];
|
let source = idsMap[edge.source];
|
||||||
let target = idsMap[edge.target];
|
let target = idsMap[edge.target];
|
||||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||||
let sourceHandle =
|
edge.sourceHandle!
|
||||||
sourceHandleSplitted[0] +
|
);
|
||||||
"|" +
|
let sourceHandle = scapedJSONStringfy({
|
||||||
source +
|
...sourceHandleObject,
|
||||||
"|" +
|
id: source,
|
||||||
sourceHandleSplitted.slice(2).join("|");
|
});
|
||||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
sourceHandleObject.id = source;
|
||||||
let targetHandle =
|
edge.data.sourceHandle = sourceHandleObject;
|
||||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
|
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||||
|
edge.targetHandle!
|
||||||
|
);
|
||||||
|
let targetHandle = scapedJSONStringfy({
|
||||||
|
...targetHandleObject,
|
||||||
|
id: target,
|
||||||
|
});
|
||||||
|
targetHandleObject.id = target;
|
||||||
|
edge.data.targetHandle = targetHandleObject;
|
||||||
let id =
|
let id =
|
||||||
"reactflow__edge-" +
|
"reactflow__edge-" +
|
||||||
source +
|
source +
|
||||||
|
|
@ -427,10 +451,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
id,
|
id,
|
||||||
style: { stroke: "#555" },
|
style: { stroke: "#555" },
|
||||||
className:
|
className:
|
||||||
targetHandle.split("|")[0] === "Text"
|
targetHandleObject.type === "Text"
|
||||||
? "stroke-gray-800 "
|
? "stroke-gray-800 "
|
||||||
: "stroke-gray-900 ",
|
: "stroke-gray-900 ",
|
||||||
animated: targetHandle.split("|")[0] === "Text",
|
animated: targetHandleObject.type === "Text",
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
edges.map((edge) => ({ ...edge, selected: false }))
|
edges.map((edge) => ({ ...edge, selected: false }))
|
||||||
|
|
@ -440,19 +464,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const addFlow = async (
|
const addFlow = async (
|
||||||
flow?: FlowType,
|
newProject: Boolean,
|
||||||
newProject?: Boolean
|
flow?: FlowType
|
||||||
): Promise<String | undefined> => {
|
): Promise<String | undefined> => {
|
||||||
if (newProject) {
|
if (newProject) {
|
||||||
let flowData = extractDataFromFlow(flow!);
|
let flowData = flow
|
||||||
if (flowData.description == "") {
|
? processDataFromFlow(flow)
|
||||||
flowData.description = getRandomDescription();
|
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new flow with a default name if no flow is provided.
|
// Create a new flow with a default name if no flow is provided.
|
||||||
const newFlow = createNewFlow(flowData, flow!);
|
const newFlow = createNewFlow(flowData, flow!);
|
||||||
processFlowEdges(newFlow);
|
|
||||||
processFlowNodes(newFlow);
|
|
||||||
|
|
||||||
const flowName = addVersionToDuplicates(newFlow, flows);
|
const flowName = addVersionToDuplicates(newFlow, flows);
|
||||||
|
|
||||||
|
|
@ -480,31 +501,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractDataFromFlow = (flow: FlowType) => {
|
const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
|
||||||
let data = flow?.data ? flow.data : null;
|
let data = flow?.data ? flow.data : null;
|
||||||
const description = flow?.description ? flow.description : "";
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
processFlowEdges(flow);
|
||||||
|
processFlowNodes(flow);
|
||||||
|
//add animation to text type edges
|
||||||
updateEdges(data.edges);
|
updateEdges(data.edges);
|
||||||
updateNodes(data.nodes, data.edges);
|
// updateNodes(data.nodes, data.edges);
|
||||||
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data, description };
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEdges = (edges: Edge[]) => {
|
const updateEdges = (edges: Edge[]) => {
|
||||||
edges.forEach((edge) => {
|
edges.forEach((edge) => {
|
||||||
|
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||||
|
edge.targetHandle!
|
||||||
|
);
|
||||||
edge.className =
|
edge.className =
|
||||||
(edge.targetHandle!.split("|")[0] === "Text"
|
(targetHandleObject.type === "Text"
|
||||||
? "stroke-gray-800 "
|
? "stroke-gray-800 "
|
||||||
: "stroke-gray-900 ") + " stroke-connection";
|
: "stroke-gray-900 ") + " stroke-connection";
|
||||||
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
|
edge.animated = targetHandleObject.type === "Text";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateNodes = (nodes: Node[], edges: Edge[]) => {
|
const updateNodes = (nodes: Node[], edges: Edge[]) => {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
|
if (node.data.node?.flow) return;
|
||||||
if (skipNodeUpdate.includes(node.data.type)) return;
|
if (skipNodeUpdate.includes(node.data.type)) return;
|
||||||
const template = templates[node.data.type];
|
const template = templates[node.data.type];
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|
@ -514,12 +540,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
if (Object.keys(template["template"]).length > 0) {
|
if (Object.keys(template["template"]).length > 0) {
|
||||||
node.data.node.base_classes = template["base_classes"];
|
node.data.node.base_classes = template["base_classes"];
|
||||||
edges.forEach((edge) => {
|
edges.forEach((edge) => {
|
||||||
|
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||||
|
edge.sourceHandle!
|
||||||
|
);
|
||||||
if (edge.source === node.id) {
|
if (edge.source === node.id) {
|
||||||
edge.sourceHandle = edge
|
let newSourceHandle = sourceHandleObject;
|
||||||
.sourceHandle!.split("|")
|
newSourceHandle.baseClasses.concat(template["base_classes"]);
|
||||||
.slice(0, 2)
|
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||||
.concat(template["base_classes"])
|
|
||||||
.join("|");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.data.node.description = template["description"];
|
node.data.node.description = template["description"];
|
||||||
|
|
@ -532,12 +559,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewFlow = (
|
const createNewFlow = (
|
||||||
flowData: { data: ReactFlowJsonObject | null; description: string },
|
flowData: ReactFlowJsonObject | null,
|
||||||
flow: FlowType
|
flow: FlowType
|
||||||
) => ({
|
) => ({
|
||||||
description: flowData.description,
|
description: flow?.description ?? getRandomDescription(),
|
||||||
name: flow?.name ?? getRandomName(),
|
name: flow?.name ?? getRandomName(),
|
||||||
data: flowData.data,
|
data: flowData,
|
||||||
id: "",
|
id: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||||
import DictComponent from "../../components/dictComponent";
|
import DictComponent from "../../components/dictComponent";
|
||||||
import Dropdown from "../../components/dropdownComponent";
|
import Dropdown from "../../components/dropdownComponent";
|
||||||
|
|
@ -166,11 +167,27 @@ const EditNodeModal = forwardRef(
|
||||||
.map((templateParam, index) => (
|
.map((templateParam, index) => (
|
||||||
<TableRow key={index} className="h-10">
|
<TableRow key={index} className="h-10">
|
||||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||||
{myData.current.node?.template[templateParam].name
|
<ShadTooltip
|
||||||
? myData.current.node.template[templateParam]
|
content={
|
||||||
.name
|
myData.current.node?.template[templateParam]
|
||||||
: myData.current.node?.template[templateParam]
|
.proxy
|
||||||
.display_name}
|
? myData.current.node?.template[
|
||||||
|
templateParam
|
||||||
|
].proxy?.id
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{myData.current.node?.template[templateParam]
|
||||||
|
.display_name
|
||||||
|
? myData.current.node.template[
|
||||||
|
templateParam
|
||||||
|
].display_name
|
||||||
|
: myData.current.node?.template[
|
||||||
|
templateParam
|
||||||
|
].name}
|
||||||
|
</span>
|
||||||
|
</ShadTooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
|
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||||
{myData.current.node?.template[templateParam]
|
{myData.current.node?.template[templateParam]
|
||||||
|
|
@ -416,6 +433,14 @@ const EditNodeModal = forwardRef(
|
||||||
.type === "prompt" ? (
|
.type === "prompt" ? (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<PromptAreaComponent
|
<PromptAreaComponent
|
||||||
|
readonly={
|
||||||
|
myData.current.node?.flow &&
|
||||||
|
myData.current.node.template[
|
||||||
|
templateParam
|
||||||
|
].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
field_name={templateParam}
|
field_name={templateParam}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -437,6 +462,14 @@ const EditNodeModal = forwardRef(
|
||||||
.type === "code" ? (
|
.type === "code" ? (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<CodeAreaComponent
|
<CodeAreaComponent
|
||||||
|
readonly={
|
||||||
|
myData.current.node?.flow &&
|
||||||
|
myData.current.node.template[
|
||||||
|
templateParam
|
||||||
|
].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
dynamic={
|
dynamic={
|
||||||
data.node!.template[templateParam]
|
data.node!.template[templateParam]
|
||||||
.dynamic ?? false
|
.dynamic ?? false
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export default function CodeAreaModal({
|
||||||
setNodeClass,
|
setNodeClass,
|
||||||
children,
|
children,
|
||||||
dynamic,
|
dynamic,
|
||||||
|
readonly = false,
|
||||||
}: codeAreaModalPropsType): JSX.Element {
|
}: codeAreaModalPropsType): JSX.Element {
|
||||||
const [code, setCode] = useState(value);
|
const [code, setCode] = useState(value);
|
||||||
const { dark } = useContext(darkContext);
|
const { dark } = useContext(darkContext);
|
||||||
|
|
@ -146,6 +147,7 @@ export default function CodeAreaModal({
|
||||||
<div className="flex h-full w-full flex-col transition-all">
|
<div className="flex h-full w-full flex-col transition-all">
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<AceEditor
|
<AceEditor
|
||||||
|
readOnly={readonly}
|
||||||
value={code}
|
value={code}
|
||||||
mode="python"
|
mode="python"
|
||||||
height={height ?? "100%"}
|
height={height ?? "100%"}
|
||||||
|
|
@ -180,7 +182,12 @@ export default function CodeAreaModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-fit w-full justify-end">
|
<div className="flex h-fit w-full justify-end">
|
||||||
<Button className="mt-3" onClick={handleClick} type="submit">
|
<Button
|
||||||
|
disabled={readonly}
|
||||||
|
className="mt-3"
|
||||||
|
onClick={handleClick}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Check & Save
|
Check & Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export default function GenericModal({
|
||||||
nodeClass,
|
nodeClass,
|
||||||
setNodeClass,
|
setNodeClass,
|
||||||
children,
|
children,
|
||||||
|
readonly = false,
|
||||||
}: genericModalPropsType): JSX.Element {
|
}: genericModalPropsType): JSX.Element {
|
||||||
const [myButtonText] = useState(buttonText);
|
const [myButtonText] = useState(buttonText);
|
||||||
const [myModalTitle] = useState(modalTitle);
|
const [myModalTitle] = useState(modalTitle);
|
||||||
|
|
@ -208,7 +209,7 @@ export default function GenericModal({
|
||||||
"flex h-full w-full"
|
"flex h-full w-full"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{type === TypeModal.PROMPT && isEdit ? (
|
{type === TypeModal.PROMPT && isEdit && !readonly ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={divRefPrompt}
|
ref={divRefPrompt}
|
||||||
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
|
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
|
||||||
|
|
@ -226,7 +227,7 @@ export default function GenericModal({
|
||||||
handleKeyDown(e, inputValue, "");
|
handleKeyDown(e, inputValue, "");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : type === TypeModal.PROMPT && !isEdit ? (
|
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
|
||||||
<SanitizedHTMLWrapper
|
<SanitizedHTMLWrapper
|
||||||
className={getClassByNumberLength()}
|
className={getClassByNumberLength()}
|
||||||
content={coloredContent}
|
content={coloredContent}
|
||||||
|
|
@ -248,6 +249,7 @@ export default function GenericModal({
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
handleKeyDown(e, value, "");
|
handleKeyDown(e, value, "");
|
||||||
}}
|
}}
|
||||||
|
readOnly={readonly}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|
@ -304,6 +306,7 @@ export default function GenericModal({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={readonly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
switch (myModalType) {
|
switch (myModalType) {
|
||||||
case TypeModal.TEXT:
|
case TypeModal.TEXT:
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import { buttonBoxPropsType } from "../../../types/components";
|
|
||||||
import { classNames } from "../../../utils/utils";
|
|
||||||
|
|
||||||
export default function ButtonBox({
|
|
||||||
onClick,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
bgColor,
|
|
||||||
textColor,
|
|
||||||
deactivate,
|
|
||||||
size,
|
|
||||||
}: buttonBoxPropsType): JSX.Element {
|
|
||||||
let bigCircle: string;
|
|
||||||
let smallCircle: string;
|
|
||||||
let titleFontSize: string;
|
|
||||||
let descriptionFontSize: string;
|
|
||||||
let padding: string;
|
|
||||||
let marginTop: string;
|
|
||||||
let height: string;
|
|
||||||
let width: string;
|
|
||||||
let textHeight: number;
|
|
||||||
let textWidth: number;
|
|
||||||
switch (size) {
|
|
||||||
case "small":
|
|
||||||
bigCircle = "h-12 w-12";
|
|
||||||
smallCircle = "h-8 w-8";
|
|
||||||
titleFontSize = "text-sm";
|
|
||||||
descriptionFontSize = "text-xs";
|
|
||||||
padding = "p-2 py-3";
|
|
||||||
marginTop = "mt-2";
|
|
||||||
height = "h-36";
|
|
||||||
width = "w-32";
|
|
||||||
break;
|
|
||||||
case "medium":
|
|
||||||
bigCircle = "h-16 w-16";
|
|
||||||
smallCircle = "h-12 w-12";
|
|
||||||
titleFontSize = "text-base";
|
|
||||||
descriptionFontSize = "text-sm";
|
|
||||||
padding = "p-4 py-5";
|
|
||||||
marginTop = "mt-3";
|
|
||||||
height = "h-44";
|
|
||||||
width = "w-36";
|
|
||||||
break;
|
|
||||||
case "big":
|
|
||||||
bigCircle = "h-20 w-20";
|
|
||||||
smallCircle = "h-16 w-16";
|
|
||||||
titleFontSize = "text-lg";
|
|
||||||
descriptionFontSize = "text-sm";
|
|
||||||
padding = "p-8 py-10";
|
|
||||||
marginTop = "mt-6";
|
|
||||||
height = "h-56";
|
|
||||||
width = "w-44";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
bigCircle = "h-20 w-20";
|
|
||||||
smallCircle = "h-16 w-16";
|
|
||||||
titleFontSize = "text-lg";
|
|
||||||
descriptionFontSize = "text-sm";
|
|
||||||
padding = "p-8 py-10";
|
|
||||||
marginTop = "mt-6";
|
|
||||||
height = "h-56";
|
|
||||||
width = "w-44";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button disabled={deactivate} onClick={onClick}>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"button-box-modal-div",
|
|
||||||
bgColor,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
padding
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-center ${bigCircle} mb-1 rounded-full bg-background/30`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-center ${smallCircle} rounded-full bg-background`}
|
|
||||||
>
|
|
||||||
<div className={textColor}>{icon}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-auto mt-auto w-full">
|
|
||||||
<h3
|
|
||||||
className={classNames(
|
|
||||||
"w-full font-semibold text-background truncate-multiline word-break-break-word",
|
|
||||||
titleFontSize,
|
|
||||||
marginTop
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
import {
|
|
||||||
ArrowLeftIcon,
|
|
||||||
ArrowUpTrayIcon,
|
|
||||||
ComputerDesktopIcon,
|
|
||||||
DocumentDuplicateIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { useContext, useRef, useState } from "react";
|
|
||||||
import LoadingComponent from "../../components/loadingComponent";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "../../components/ui/dialog";
|
|
||||||
import { IMPORT_DIALOG_SUBTITLE } from "../../constants/constants";
|
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
|
||||||
import { getExamples } from "../../controllers/API";
|
|
||||||
import { FlowType } from "../../types/flow";
|
|
||||||
import { classNames } from "../../utils/utils";
|
|
||||||
import ButtonBox from "./buttonBox";
|
|
||||||
|
|
||||||
export default function ImportModal(): JSX.Element {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
const { setErrorData } = useContext(alertContext);
|
|
||||||
const ref = useRef();
|
|
||||||
const [showExamples, setShowExamples] = useState(false);
|
|
||||||
const [loadingExamples, setLoadingExamples] = useState(false);
|
|
||||||
const [examples, setExamples] = useState<FlowType[]>([]);
|
|
||||||
const { uploadFlow, addFlow } = useContext(TabsContext);
|
|
||||||
|
|
||||||
function handleExamples(): void {
|
|
||||||
setLoadingExamples(true);
|
|
||||||
getExamples()
|
|
||||||
.then((result) => {
|
|
||||||
setLoadingExamples(false);
|
|
||||||
setExamples(result);
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
setErrorData({
|
|
||||||
title: "there was an error loading examples, please try again",
|
|
||||||
list: [error.message],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
|
||||||
<DialogTrigger></DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
className={classNames(
|
|
||||||
showExamples
|
|
||||||
? "h-[600px] lg:max-w-[650px]"
|
|
||||||
: "h-[450px] lg:max-w-[650px]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center">
|
|
||||||
{showExamples && (
|
|
||||||
<>
|
|
||||||
<div className="dialog-header-modal-div">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="dialog-header-modal-button disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
|
||||||
onClick={() => {
|
|
||||||
setShowExamples(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon
|
|
||||||
className="ml-1 h-5 w-5 text-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
|
|
||||||
{showExamples ? "Select an example" : "Import"}
|
|
||||||
</span>
|
|
||||||
<ArrowUpTrayIcon
|
|
||||||
className="ml-1 h-5 w-5 text-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"dialog-modal-examples-div",
|
|
||||||
showExamples && !loadingExamples
|
|
||||||
? "dialog-modal-example-true"
|
|
||||||
: "dialog-modal-example-false"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!showExamples && (
|
|
||||||
<div className="dialog-modal-button-box-div">
|
|
||||||
<ButtonBox
|
|
||||||
size="big"
|
|
||||||
bgColor="bg-medium-emerald "
|
|
||||||
description="Prebuilt Examples"
|
|
||||||
icon={<DocumentDuplicateIcon className="document-icon" />}
|
|
||||||
onClick={() => {
|
|
||||||
setShowExamples(true);
|
|
||||||
handleExamples();
|
|
||||||
}}
|
|
||||||
textColor="text-medium-emerald "
|
|
||||||
title="Examples"
|
|
||||||
></ButtonBox>
|
|
||||||
<ButtonBox
|
|
||||||
size="big"
|
|
||||||
bgColor="bg-almost-dark-blue "
|
|
||||||
description="Import from Local"
|
|
||||||
icon={<ComputerDesktopIcon className="document-icon" />}
|
|
||||||
onClick={() => {
|
|
||||||
uploadFlow();
|
|
||||||
setModalOpen(false);
|
|
||||||
}}
|
|
||||||
textColor="text-almost-dark-blue "
|
|
||||||
title="Local File"
|
|
||||||
></ButtonBox>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showExamples && loadingExamples && (
|
|
||||||
<div className="loading-component-div">
|
|
||||||
<LoadingComponent remSize={30} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showExamples &&
|
|
||||||
!loadingExamples &&
|
|
||||||
examples.map((example, index) => {
|
|
||||||
return (
|
|
||||||
<div key={example.name} className="m-2">
|
|
||||||
{" "}
|
|
||||||
<ButtonBox
|
|
||||||
size="small"
|
|
||||||
bgColor="bg-medium-emerald "
|
|
||||||
description={example.description ?? "Prebuilt Examples"}
|
|
||||||
icon={
|
|
||||||
<DocumentDuplicateIcon
|
|
||||||
strokeWidth={1.5}
|
|
||||||
className="h-6 w-6 flex-shrink-0"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
addFlow(example, false);
|
|
||||||
setModalOpen(false);
|
|
||||||
}}
|
|
||||||
textColor="text-medium-emerald "
|
|
||||||
title={example.name}
|
|
||||||
></ButtonBox>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="dialog-modal-footer">
|
|
||||||
<a
|
|
||||||
href="https://github.com/logspace-ai/langflow_examples"
|
|
||||||
target="_blank"
|
|
||||||
className="dialog-modal-footer-link "
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
viewBox="0 0 98 96"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="ml-2 ">Langflow Examples</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default function CommunityPage(): JSX.Element {
|
||||||
size="sm"
|
size="sm"
|
||||||
className="whitespace-nowrap "
|
className="whitespace-nowrap "
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addFlow(flow, true).then((id) => {
|
addFlow(true, flow).then((id) => {
|
||||||
navigate("/flow/" + id);
|
navigate("/flow/" + id);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export default function DisclosureComponent({
|
||||||
<div>
|
<div>
|
||||||
<Disclosure.Button className="components-disclosure-arrangement">
|
<Disclosure.Button className="components-disclosure-arrangement">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
|
{/* BUG ON THIS ICON */}
|
||||||
<Icon strokeWidth={1.5} size={22} className="text-primary" />
|
<Icon strokeWidth={1.5} size={22} className="text-primary" />
|
||||||
<span className="components-disclosure-title">{title}</span>
|
<span className="components-disclosure-title">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,18 @@ import { TabsContext } from "../../../../contexts/tabsContext";
|
||||||
import { typesContext } from "../../../../contexts/typesContext";
|
import { typesContext } from "../../../../contexts/typesContext";
|
||||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||||
import { APIClassType } from "../../../../types/api";
|
import { APIClassType } from "../../../../types/api";
|
||||||
import { FlowType, NodeType } from "../../../../types/flow";
|
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
|
||||||
import { TabsState } from "../../../../types/tabs";
|
import { TabsState } from "../../../../types/tabs";
|
||||||
import { isValidConnection } from "../../../../utils/reactflowUtils";
|
import {
|
||||||
import { isWrappedWithClass } from "../../../../utils/utils";
|
generateFlow,
|
||||||
|
generateNodeFromFlow,
|
||||||
|
isValidConnection,
|
||||||
|
scapeJSONParse,
|
||||||
|
validateSelection,
|
||||||
|
} from "../../../../utils/reactflowUtils";
|
||||||
|
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||||
|
import SelectionMenu from "../SelectionMenuComponent";
|
||||||
import ExtraSidebar from "../extraSidebarComponent";
|
import ExtraSidebar from "../extraSidebarComponent";
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
|
|
@ -238,12 +245,19 @@ export default function Page({
|
||||||
addEdge(
|
addEdge(
|
||||||
{
|
{
|
||||||
...params,
|
...params,
|
||||||
|
data: {
|
||||||
|
targetHandle: scapeJSONParse(params.targetHandle!),
|
||||||
|
sourceHandle: scapeJSONParse(params.sourceHandle!),
|
||||||
|
},
|
||||||
style: { stroke: "#555" },
|
style: { stroke: "#555" },
|
||||||
className:
|
className:
|
||||||
(params.targetHandle?.split("|")[0] === "Text"
|
((scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||||
|
.type === "Text"
|
||||||
? "stroke-foreground "
|
? "stroke-foreground "
|
||||||
: "stroke-foreground ") + " stroke-connection",
|
: "stroke-foreground ") + " stroke-connection",
|
||||||
animated: params.targetHandle?.split("|")[0] === "Text",
|
animated:
|
||||||
|
(scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||||
|
.type === "Text",
|
||||||
},
|
},
|
||||||
eds
|
eds
|
||||||
)
|
)
|
||||||
|
|
@ -394,7 +408,7 @@ export default function Page({
|
||||||
edgeUpdateSuccessful.current = true;
|
edgeUpdateSuccessful.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectionEnded, setSelectionEnded] = useState(false);
|
const [selectionEnded, setSelectionEnded] = useState(true);
|
||||||
|
|
||||||
const onSelectionEnd = useCallback(() => {
|
const onSelectionEnd = useCallback(() => {
|
||||||
setSelectionEnded(true);
|
setSelectionEnded(true);
|
||||||
|
|
@ -481,6 +495,50 @@ export default function Page({
|
||||||
[&>button]:border-b-border hover:[&>button]:bg-border"
|
[&>button]:border-b-border hover:[&>button]:bg-border"
|
||||||
></Controls>
|
></Controls>
|
||||||
)}
|
)}
|
||||||
|
<SelectionMenu
|
||||||
|
isVisible={selectionMenuVisible}
|
||||||
|
nodes={lastSelection?.nodes}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
validateSelection(lastSelection!, edges).length === 0
|
||||||
|
) {
|
||||||
|
const { newFlow } = generateFlow(
|
||||||
|
lastSelection!,
|
||||||
|
reactFlowInstance!,
|
||||||
|
getRandomName()
|
||||||
|
);
|
||||||
|
const newGroupNode = generateNodeFromFlow(
|
||||||
|
newFlow,
|
||||||
|
getNodeId
|
||||||
|
);
|
||||||
|
setNodes((oldNodes) => [
|
||||||
|
...oldNodes.filter(
|
||||||
|
(oldNodes) =>
|
||||||
|
!lastSelection?.nodes.some(
|
||||||
|
(selectionNode) =>
|
||||||
|
selectionNode.id === oldNodes.id
|
||||||
|
)
|
||||||
|
),
|
||||||
|
newGroupNode,
|
||||||
|
]);
|
||||||
|
setEdges((oldEdges) =>
|
||||||
|
oldEdges.filter(
|
||||||
|
(oldEdge) =>
|
||||||
|
!lastSelection!.nodes.some(
|
||||||
|
(selectionNode) =>
|
||||||
|
selectionNode.id === oldEdge.target ||
|
||||||
|
selectionNode.id === oldEdge.source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Invalid selection",
|
||||||
|
list: validateSelection(lastSelection!, edges),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
{!view && (
|
{!view && (
|
||||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />
|
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { NodeToolbar } from "reactflow";
|
||||||
|
import IconComponent from "../../../../components/genericIconComponent";
|
||||||
|
export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
const [lastNodes, setLastNodes] = useState(nodes);
|
||||||
|
|
||||||
|
// nodes get saved to not be gone after the toolbar closes
|
||||||
|
useEffect(() => {
|
||||||
|
setLastNodes(nodes);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// transition starts after and ends before the toolbar closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
setIsOpen(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeToolbar
|
||||||
|
isVisible={isOpen}
|
||||||
|
offset={5}
|
||||||
|
nodeId={
|
||||||
|
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="h-10 w-28 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"h-10 w-24 rounded-md border border-indigo-300 bg-white px-2.5 text-gray-700 shadow-inner transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300" +
|
||||||
|
(isTransitioning ? " translate-y-0" : " translate-y-10")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<IconComponent name="Group" className="w-6" />
|
||||||
|
Group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NodeToolbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ export default function ExtraSidebar(): JSX.Element {
|
||||||
<button
|
<button
|
||||||
className="extra-side-bar-buttons"
|
className="extra-side-bar-buttons"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
uploadFlow();
|
uploadFlow(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent name="FileUp" className="side-bar-button-size " />
|
<IconComponent name="FileUp" className="side-bar-button-size " />
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,17 @@ import {
|
||||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||||
import EditNodeModal from "../../../../modals/EditNodeModal";
|
import EditNodeModal from "../../../../modals/EditNodeModal";
|
||||||
import { nodeToolbarPropsType } from "../../../../types/components";
|
import { nodeToolbarPropsType } from "../../../../types/components";
|
||||||
|
import {
|
||||||
|
expandGroupNode,
|
||||||
|
updateFlowPosition,
|
||||||
|
} from "../../../../utils/reactflowUtils";
|
||||||
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
|
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
|
||||||
|
|
||||||
export default function NodeToolbarComponent({
|
export default function NodeToolbarComponent({
|
||||||
data,
|
data,
|
||||||
setData,
|
setData,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
|
position,
|
||||||
setShowNode,
|
setShowNode,
|
||||||
numberOfHandles,
|
numberOfHandles,
|
||||||
showNode,
|
showNode,
|
||||||
|
|
@ -47,6 +52,8 @@ export default function NodeToolbarComponent({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const isMinimal = canMinimize();
|
const isMinimal = canMinimize();
|
||||||
|
const isGroup = data.node?.flow ? true : false;
|
||||||
|
|
||||||
const { paste } = useContext(TabsContext);
|
const { paste } = useContext(TabsContext);
|
||||||
const reactFlowInstance = useReactFlow();
|
const reactFlowInstance = useReactFlow();
|
||||||
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
||||||
|
|
@ -65,6 +72,10 @@ export default function NodeToolbarComponent({
|
||||||
if (event.includes("disabled")) {
|
if (event.includes("disabled")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.includes("ungroup")) {
|
||||||
|
updateFlowPosition(position, data.node?.flow!);
|
||||||
|
expandGroupNode(data, reactFlowInstance);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -134,7 +145,7 @@ export default function NodeToolbarComponent({
|
||||||
</a>
|
</a>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
|
|
||||||
{isMinimal ? (
|
{isMinimal || isGroup ? (
|
||||||
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
||||||
<ShadTooltip content="More" side="top">
|
<ShadTooltip content="More" side="top">
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -189,6 +200,17 @@ export default function NodeToolbarComponent({
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
|
{isGroup && (
|
||||||
|
<SelectItem value={getRandomKeyByssmm() + "ungroup"}>
|
||||||
|
<div className="flex">
|
||||||
|
<IconComponent
|
||||||
|
name="Ungroup"
|
||||||
|
className="relative top-0.5 mr-2 h-4 w-4"
|
||||||
|
/>{" "}
|
||||||
|
Ungroup{" "}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -223,29 +245,6 @@ export default function NodeToolbarComponent({
|
||||||
<></>
|
<></>
|
||||||
</EditNodeModal>
|
</EditNodeModal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*
|
|
||||||
<ShadTooltip content="Edit" side="top">
|
|
||||||
<div>
|
|
||||||
<EditNodeModal
|
|
||||||
data={data}
|
|
||||||
setData={setData}
|
|
||||||
nodeLength={nodeLength}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
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" +
|
|
||||||
(!canMinimize() && " rounded-r-md ") +
|
|
||||||
(nodeLength == 0
|
|
||||||
? " text-muted-foreground"
|
|
||||||
: " text-foreground")
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconComponent name="Settings2" className="h-4 w-4 " />
|
|
||||||
</div>
|
|
||||||
</EditNodeModal>
|
|
||||||
</div>
|
|
||||||
</ShadTooltip> */}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ export default function HomePage(): JSX.Element {
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
firstButtonName="New Project"
|
firstButtonName="New Project"
|
||||||
onFirstBtnClick={() => {
|
onFirstBtnClick={() => {
|
||||||
addFlow(null!, true).then((id) => {
|
addFlow(true).then((id) => {
|
||||||
navigate("/flow/" + id);
|
navigate("/flow/" + id);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Edge, Node, Viewport } from "reactflow";
|
import { Edge, Node, Viewport } from "reactflow";
|
||||||
|
import { FlowType } from "../flow";
|
||||||
//kind and class are just representative names to represent the actual structure of the object received by the API
|
//kind and class are just representative names to represent the actual structure of the object received by the API
|
||||||
export type APIDataType = { [key: string]: APIKindType };
|
export type APIDataType = { [key: string]: APIKindType };
|
||||||
export type APIObjectType = { kind: APIKindType; [key: string]: APIKindType };
|
export type APIObjectType = { kind: APIKindType; [key: string]: APIKindType };
|
||||||
export type APIKindType = { class: APIClassType; [key: string]: APIClassType };
|
export type APIKindType = { class: APIClassType; [key: string]: APIClassType };
|
||||||
export type APITemplateType = {
|
export type APITemplateType = {
|
||||||
variable: TemplateVariableType;
|
|
||||||
[key: string]: TemplateVariableType;
|
[key: string]: TemplateVariableType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -23,10 +23,13 @@ export type APIClassType = {
|
||||||
beta?: boolean;
|
beta?: boolean;
|
||||||
documentation: string;
|
documentation: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
flow?: FlowType;
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| Array<string>
|
| Array<string>
|
||||||
| string
|
| string
|
||||||
| APITemplateType
|
| APITemplateType
|
||||||
|
| boolean
|
||||||
|
| FlowType
|
||||||
| CustomFieldsType
|
| CustomFieldsType
|
||||||
| boolean
|
| boolean
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -38,8 +41,11 @@ export type TemplateVariableType = {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
list: boolean;
|
list: boolean;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
readonly: boolean;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
value?: any;
|
value?: any;
|
||||||
|
dynamic?: boolean;
|
||||||
|
proxy?: { id: string; field: string };
|
||||||
input_types?: Array<string>;
|
input_types?: Array<string>;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { ReactElement, ReactNode } from "react";
|
import { ReactElement, ReactNode } from "react";
|
||||||
import { ReactFlowJsonObject } from "reactflow";
|
import { ReactFlowJsonObject, XYPosition } from "reactflow";
|
||||||
import { APIClassType, APITemplateType, TemplateVariableType } from "../api";
|
import { APIClassType, APITemplateType, TemplateVariableType } from "../api";
|
||||||
import { ChatMessageType } from "../chat";
|
import { ChatMessageType } from "../chat";
|
||||||
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
|
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
|
||||||
import { typesContextType } from "../typesContext";
|
import { typesContextType } from "../typesContext";
|
||||||
|
import { sourceHandleType, targetHandleType } from "./../flow/index";
|
||||||
export type InputComponentType = {
|
export type InputComponentType = {
|
||||||
|
autoFocus?: boolean;
|
||||||
|
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
|
@ -17,6 +19,7 @@ export type InputComponentType = {
|
||||||
showPass?: boolean;
|
showPass?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
blurOnEnter?: boolean;
|
||||||
};
|
};
|
||||||
export type ToggleComponentType = {
|
export type ToggleComponentType = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
@ -37,7 +40,7 @@ export type ParameterComponentType = {
|
||||||
data: NodeDataType;
|
data: NodeDataType;
|
||||||
setData: (value: NodeDataType) => void;
|
setData: (value: NodeDataType) => void;
|
||||||
title: string;
|
title: string;
|
||||||
id: string;
|
id: sourceHandleType | targetHandleType;
|
||||||
color: string;
|
color: string;
|
||||||
left: boolean;
|
left: boolean;
|
||||||
type: string | undefined;
|
type: string | undefined;
|
||||||
|
|
@ -47,6 +50,7 @@ export type ParameterComponentType = {
|
||||||
dataContext?: typesContextType;
|
dataContext?: typesContextType;
|
||||||
optionalHandle?: Array<String> | null;
|
optionalHandle?: Array<String> | null;
|
||||||
info?: string;
|
info?: string;
|
||||||
|
proxy?: { field: string; id: string };
|
||||||
showNode?: boolean;
|
showNode?: boolean;
|
||||||
};
|
};
|
||||||
export type InputListComponentType = {
|
export type InputListComponentType = {
|
||||||
|
|
@ -81,6 +85,7 @@ export type TextAreaComponentType = {
|
||||||
onChange: (value: string[] | string) => void;
|
onChange: (value: string[] | string) => void;
|
||||||
value: string;
|
value: string;
|
||||||
editNode?: boolean;
|
editNode?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PromptAreaComponentType = {
|
export type PromptAreaComponentType = {
|
||||||
|
|
@ -90,6 +95,7 @@ export type PromptAreaComponentType = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onChange: (value: string[] | string) => void;
|
onChange: (value: string[] | string) => void;
|
||||||
value: string;
|
value: string;
|
||||||
|
readonly?: boolean;
|
||||||
editNode?: boolean;
|
editNode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,6 +107,7 @@ export type CodeAreaComponentType = {
|
||||||
nodeClass?: APIClassType;
|
nodeClass?: APIClassType;
|
||||||
setNodeClass?: (value: APIClassType) => void;
|
setNodeClass?: (value: APIClassType) => void;
|
||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileComponentType = {
|
export type FileComponentType = {
|
||||||
|
|
@ -427,6 +434,7 @@ export type nodeToolbarPropsType = {
|
||||||
data: NodeDataType;
|
data: NodeDataType;
|
||||||
deleteNode: (idx: string) => void;
|
deleteNode: (idx: string) => void;
|
||||||
setData: (newState: NodeDataType) => void;
|
setData: (newState: NodeDataType) => void;
|
||||||
|
position: XYPosition;
|
||||||
setShowNode: (boolean: any) => void;
|
setShowNode: (boolean: any) => void;
|
||||||
numberOfHandles: boolean[] | [];
|
numberOfHandles: boolean[] | [];
|
||||||
showNode: boolean;
|
showNode: boolean;
|
||||||
|
|
@ -462,6 +470,7 @@ export type codeAreaModalPropsType = {
|
||||||
setNodeClass: (Class: APIClassType) => void | undefined;
|
setNodeClass: (Class: APIClassType) => void | undefined;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type chatMessagePropsType = {
|
export type chatMessagePropsType = {
|
||||||
|
|
@ -486,6 +495,7 @@ export type genericModalPropsType = {
|
||||||
nodeClass?: APIClassType;
|
nodeClass?: APIClassType;
|
||||||
setNodeClass?: (Class: APIClassType) => void;
|
setNodeClass?: (Class: APIClassType) => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type buttonBoxPropsType = {
|
export type buttonBoxPropsType = {
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ export type NodeType = {
|
||||||
type?: string;
|
type?: string;
|
||||||
position: XYPosition;
|
position: XYPosition;
|
||||||
data: NodeDataType;
|
data: NodeDataType;
|
||||||
|
selected?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeDataType = {
|
export type NodeDataType = {
|
||||||
type: string;
|
type: string;
|
||||||
node?: APIClassType;
|
node?: APIClassType;
|
||||||
id: string;
|
id: string;
|
||||||
|
output_types?: string[];
|
||||||
};
|
};
|
||||||
// FlowStyleType is the type of the style object that is used to style the
|
// FlowStyleType is the type of the style object that is used to style the
|
||||||
// Flow card with an emoji and a color.
|
// Flow card with an emoji and a color.
|
||||||
|
|
@ -35,3 +37,18 @@ export type TweaksType = Array<
|
||||||
};
|
};
|
||||||
} & FlowStyleType
|
} & FlowStyleType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// right side
|
||||||
|
export type sourceHandleType = {
|
||||||
|
dataType: string;
|
||||||
|
id: string;
|
||||||
|
baseClasses: string[];
|
||||||
|
};
|
||||||
|
//left side
|
||||||
|
export type targetHandleType = {
|
||||||
|
inputTypes?: string[];
|
||||||
|
type: string;
|
||||||
|
fieldName: string;
|
||||||
|
id: string;
|
||||||
|
proxy?: { field: string; id: string };
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ export type TabsContextType = {
|
||||||
flows: Array<FlowType>;
|
flows: Array<FlowType>;
|
||||||
removeFlow: (id: string) => void;
|
removeFlow: (id: string) => void;
|
||||||
addFlow: (
|
addFlow: (
|
||||||
flow?: FlowType,
|
newProject: boolean,
|
||||||
newProject?: Boolean
|
flow?: FlowType
|
||||||
) => Promise<String | undefined>;
|
) => Promise<String | undefined>;
|
||||||
updateFlow: (newFlow: FlowType) => void;
|
updateFlow: (newFlow: FlowType) => void;
|
||||||
incrementNodeId: () => string;
|
incrementNodeId: () => string;
|
||||||
|
|
@ -23,7 +23,7 @@ export type TabsContextType = {
|
||||||
uploadFlows: () => void;
|
uploadFlows: () => void;
|
||||||
isBuilt: boolean;
|
isBuilt: boolean;
|
||||||
setIsBuilt: (state: boolean) => void;
|
setIsBuilt: (state: boolean) => void;
|
||||||
uploadFlow: (newFlow?: boolean, file?: File) => Promise<String | undefined>;
|
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
|
||||||
hardReset: () => void;
|
hardReset: () => void;
|
||||||
getNodeId: (nodeType: string) => string;
|
getNodeId: (nodeType: string) => string;
|
||||||
tabsState: TabsState;
|
tabsState: TabsState;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Edge, Node } from "reactflow";
|
import { Edge, Node } from "reactflow";
|
||||||
import { NodeType } from "../flow";
|
import { FlowType, NodeType } from "../flow";
|
||||||
|
|
||||||
export type cleanEdgesType = {
|
export type cleanEdgesType = {
|
||||||
flow: {
|
flow: {
|
||||||
|
|
@ -11,5 +11,17 @@ export type cleanEdgesType = {
|
||||||
|
|
||||||
export type unselectAllNodesType = {
|
export type unselectAllNodesType = {
|
||||||
updateNodes: (nodes: Node[]) => void;
|
updateNodes: (nodes: Node[]) => void;
|
||||||
data: Node[] | null;
|
data: Node[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type updateEdgesHandleIdsType = {
|
||||||
|
nodes: NodeType[];
|
||||||
|
edges: Edge[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type generateFlowType = { newFlow: FlowType; removedEdges: Edge[] };
|
||||||
|
|
||||||
|
export type findLastNodeType = {
|
||||||
|
nodes: NodeType[];
|
||||||
|
edges: Edge[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,30 @@ import {
|
||||||
Connection,
|
Connection,
|
||||||
Edge,
|
Edge,
|
||||||
Node,
|
Node,
|
||||||
|
OnSelectionChangeParams,
|
||||||
ReactFlowInstance,
|
ReactFlowInstance,
|
||||||
ReactFlowJsonObject,
|
ReactFlowJsonObject,
|
||||||
|
XYPosition,
|
||||||
} from "reactflow";
|
} from "reactflow";
|
||||||
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { specialCharsRegex } from "../constants/constants";
|
import { specialCharsRegex } from "../constants/constants";
|
||||||
import { APITemplateType } from "../types/api";
|
import { APITemplateType, TemplateVariableType } from "../types/api";
|
||||||
import { FlowType, NodeType } from "../types/flow";
|
import {
|
||||||
|
FlowType,
|
||||||
|
NodeDataType,
|
||||||
|
NodeType,
|
||||||
|
sourceHandleType,
|
||||||
|
targetHandleType,
|
||||||
|
} from "../types/flow";
|
||||||
import {
|
import {
|
||||||
cleanEdgesType,
|
cleanEdgesType,
|
||||||
|
findLastNodeType,
|
||||||
|
generateFlowType,
|
||||||
unselectAllNodesType,
|
unselectAllNodesType,
|
||||||
|
updateEdgesHandleIdsType,
|
||||||
} from "../types/utils/reactflowUtils";
|
} from "../types/utils/reactflowUtils";
|
||||||
import { toNormalCase } from "./utils";
|
import { toNormalCase, toTitleCase } from "./utils";
|
||||||
|
const uid = new ShortUniqueId({ length: 5 });
|
||||||
|
|
||||||
export function cleanEdges({
|
export function cleanEdges({
|
||||||
flow: { edges, nodes },
|
flow: { edges, nodes },
|
||||||
|
|
@ -32,26 +45,30 @@ export function cleanEdges({
|
||||||
const sourceHandle = edge.sourceHandle; //right
|
const sourceHandle = edge.sourceHandle; //right
|
||||||
const targetHandle = edge.targetHandle; //left
|
const targetHandle = edge.targetHandle; //left
|
||||||
if (targetHandle) {
|
if (targetHandle) {
|
||||||
const field = targetHandle.split("|")[1];
|
const targetHandleObject: targetHandleType =
|
||||||
const id =
|
scapeJSONParse(targetHandle);
|
||||||
(targetNode.data.node?.template[field]?.input_types?.join(";") ??
|
const field = targetHandleObject.fieldName;
|
||||||
targetNode.data.node?.template[field]?.type) +
|
const id: targetHandleType = {
|
||||||
"|" +
|
type: targetNode.data.node!.template[field]?.type,
|
||||||
field +
|
fieldName: field,
|
||||||
"|" +
|
id: targetNode.data.id,
|
||||||
targetNode.data.id;
|
inputTypes: targetNode.data.node!.template[field]?.input_types,
|
||||||
if (id !== targetHandle) {
|
};
|
||||||
|
if (targetNode.data.node!.template[field]?.proxy) {
|
||||||
|
id.proxy = targetNode.data.node!.template[field]?.proxy;
|
||||||
|
}
|
||||||
|
if (scapedJSONStringfy(id) !== targetHandle) {
|
||||||
newEdges = newEdges.filter((e) => e.id !== edge.id);
|
newEdges = newEdges.filter((e) => e.id !== edge.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sourceHandle) {
|
if (sourceHandle) {
|
||||||
const id = [
|
const id: sourceHandleType = {
|
||||||
sourceNode.data.type,
|
id: sourceNode.data.id,
|
||||||
sourceNode.data.id,
|
baseClasses: sourceNode.data.node!.base_classes,
|
||||||
...sourceNode.data.node?.base_classes!,
|
dataType: sourceNode.data.type,
|
||||||
].join("|");
|
};
|
||||||
if (id !== sourceHandle) {
|
if (scapedJSONStringfy(id) !== sourceHandle) {
|
||||||
newEdges = newEdges.filter((edg) => edg.id !== edge.id);
|
newEdges = newEdges.filter((e) => e.id !== edge.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +78,7 @@ export function cleanEdges({
|
||||||
|
|
||||||
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
|
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
|
||||||
let newNodes = _.cloneDeep(data);
|
let newNodes = _.cloneDeep(data);
|
||||||
newNodes!.forEach((node: Node) => {
|
newNodes.forEach((node: Node) => {
|
||||||
node.selected = false;
|
node.selected = false;
|
||||||
});
|
});
|
||||||
updateNodes(newNodes!);
|
updateNodes(newNodes!);
|
||||||
|
|
@ -71,21 +88,18 @@ export function isValidConnection(
|
||||||
{ source, target, sourceHandle, targetHandle }: Connection,
|
{ source, target, sourceHandle, targetHandle }: Connection,
|
||||||
reactFlowInstance: ReactFlowInstance
|
reactFlowInstance: ReactFlowInstance
|
||||||
) {
|
) {
|
||||||
|
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
|
||||||
|
const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);
|
||||||
if (
|
if (
|
||||||
targetHandle
|
targetHandleObject.inputTypes?.some(
|
||||||
?.split("|")[0]
|
(n) => n === sourceHandleObject.dataType
|
||||||
.split(";")
|
) ||
|
||||||
.some((target) => target === sourceHandle?.split("|")[0]) ||
|
sourceHandleObject.baseClasses.some(
|
||||||
sourceHandle
|
(t) =>
|
||||||
?.split("|")
|
targetHandleObject.inputTypes?.some((n) => n === t) ||
|
||||||
.slice(2)
|
t === targetHandleObject.type
|
||||||
.some((target) =>
|
) ||
|
||||||
targetHandle
|
targetHandleObject.type === "str"
|
||||||
?.split("|")[0]
|
|
||||||
.split(";")
|
|
||||||
.some((n) => n === target)
|
|
||||||
) ||
|
|
||||||
targetHandle?.split("|")[0] === "str"
|
|
||||||
) {
|
) {
|
||||||
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
|
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
|
|
@ -97,11 +111,11 @@ export function isValidConnection(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(!targetNode.template[targetHandle?.split("|")[1]!].list &&
|
(!targetNode.template[targetHandleObject.fieldName].list &&
|
||||||
!reactFlowInstance
|
!reactFlowInstance
|
||||||
.getEdges()
|
.getEdges()
|
||||||
.find((e) => e.targetHandle === targetHandle)) ||
|
.find((e) => e.targetHandle === targetHandle)) ||
|
||||||
targetNode.template[targetHandle?.split("|")[1]!].list
|
targetNode.template[targetHandleObject.fieldName].list
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -152,26 +166,36 @@ export function updateIds(
|
||||||
|
|
||||||
newFlow.nodes.forEach((node: NodeType) => {
|
newFlow.nodes.forEach((node: NodeType) => {
|
||||||
// Generate a unique node ID
|
// Generate a unique node ID
|
||||||
let newId = getNodeId(node.data.type);
|
let newId = getNodeId(node.data.node?.flow ? "GroupNode" : node.data.type);
|
||||||
idsMap[node.id] = newId;
|
idsMap[node.id] = newId;
|
||||||
node.id = newId;
|
node.id = newId;
|
||||||
node.data.id = newId;
|
node.data.id = newId;
|
||||||
// Add the new node to the list of nodes in state
|
// Add the new node to the list of nodes in state
|
||||||
});
|
});
|
||||||
|
|
||||||
newFlow.edges.forEach((edge) => {
|
newFlow.edges.forEach((edge: Edge) => {
|
||||||
edge.source = idsMap[edge.source];
|
edge.source = idsMap[edge.source];
|
||||||
edge.target = idsMap[edge.target];
|
edge.target = idsMap[edge.target];
|
||||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||||
edge.sourceHandle =
|
edge.sourceHandle!
|
||||||
sourceHandleSplitted[0] +
|
);
|
||||||
"|" +
|
edge.sourceHandle = scapedJSONStringfy({
|
||||||
edge.source +
|
...sourceHandleObject,
|
||||||
"|" +
|
id: edge.source,
|
||||||
sourceHandleSplitted.slice(2).join("|");
|
});
|
||||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
if (edge.data?.sourceHandle?.id) {
|
||||||
edge.targetHandle =
|
edge.data.sourceHandle.id = edge.source;
|
||||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + edge.target;
|
}
|
||||||
|
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||||
|
edge.targetHandle!
|
||||||
|
);
|
||||||
|
edge.targetHandle = scapedJSONStringfy({
|
||||||
|
...targetHandleObject,
|
||||||
|
id: edge.target,
|
||||||
|
});
|
||||||
|
if (edge.data?.targetHandle?.id) {
|
||||||
|
edge.data.targetHandle.id = edge.target;
|
||||||
|
}
|
||||||
edge.id =
|
edge.id =
|
||||||
"reactflow__edge-" +
|
"reactflow__edge-" +
|
||||||
edge.source +
|
edge.source +
|
||||||
|
|
@ -210,8 +234,10 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
||||||
template[t].value === "") &&
|
template[t].value === "") &&
|
||||||
!edges.some(
|
!edges.some(
|
||||||
(edge) =>
|
(edge) =>
|
||||||
edge.targetHandle?.split("|")[1] === t &&
|
(scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
|
||||||
edge.targetHandle.split("|")[2] === node.id
|
t &&
|
||||||
|
(scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
|
||||||
|
node.id
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
errors.push(
|
errors.push(
|
||||||
|
|
@ -266,6 +292,49 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
|
||||||
return newName;
|
return newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateEdgesHandleIds({
|
||||||
|
edges,
|
||||||
|
nodes,
|
||||||
|
}: updateEdgesHandleIdsType): Edge[] {
|
||||||
|
let newEdges = _.cloneDeep(edges);
|
||||||
|
newEdges.forEach((edge) => {
|
||||||
|
const sourceNodeId = edge.source;
|
||||||
|
const targetNodeId = edge.target;
|
||||||
|
const sourceNode = nodes.find((node) => node.id === sourceNodeId);
|
||||||
|
const targetNode = nodes.find((node) => node.id === targetNodeId);
|
||||||
|
let source = edge.sourceHandle;
|
||||||
|
let target = edge.targetHandle;
|
||||||
|
//right
|
||||||
|
let newSource: sourceHandleType;
|
||||||
|
//left
|
||||||
|
let newTarget: targetHandleType;
|
||||||
|
if (target && targetNode) {
|
||||||
|
let field = target.split("|")[1];
|
||||||
|
newTarget = {
|
||||||
|
type: targetNode.data.node!.template[field].type,
|
||||||
|
fieldName: field,
|
||||||
|
id: targetNode.data.id,
|
||||||
|
inputTypes: targetNode.data.node!.template[field].input_types,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (source && sourceNode) {
|
||||||
|
newSource = {
|
||||||
|
id: sourceNode.data.id,
|
||||||
|
baseClasses: sourceNode.data.node!.base_classes,
|
||||||
|
dataType: sourceNode.data.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
edge.sourceHandle = scapedJSONStringfy(newSource!);
|
||||||
|
edge.targetHandle = scapedJSONStringfy(newTarget!);
|
||||||
|
const newData = {
|
||||||
|
sourceHandle: scapeJSONParse(edge.sourceHandle),
|
||||||
|
targetHandle: scapeJSONParse(edge.targetHandle),
|
||||||
|
};
|
||||||
|
edge.data = newData;
|
||||||
|
});
|
||||||
|
return newEdges;
|
||||||
|
}
|
||||||
|
|
||||||
export function handleKeyDown(
|
export function handleKeyDown(
|
||||||
e:
|
e:
|
||||||
| React.KeyboardEvent<HTMLInputElement>
|
| React.KeyboardEvent<HTMLInputElement>
|
||||||
|
|
@ -377,3 +446,592 @@ export function convertValuesToNumbers(arr) {
|
||||||
return newObj;
|
return newObj;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function scapedJSONStringfy(json: object): string {
|
||||||
|
return customStringify(json).replace(/"/g, "œ");
|
||||||
|
}
|
||||||
|
export function scapeJSONParse(json: string): any {
|
||||||
|
let parsed = json.replace(/œ/g, '"');
|
||||||
|
return JSON.parse(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function receives an array of edges and return true if any of the handles are not a json string
|
||||||
|
export function checkOldEdgesHandles(edges: Edge[]): boolean {
|
||||||
|
return edges.some(
|
||||||
|
(edge) =>
|
||||||
|
!edge.sourceHandle ||
|
||||||
|
!edge.targetHandle ||
|
||||||
|
!edge.sourceHandle.includes("{") ||
|
||||||
|
!edge.targetHandle.includes("{")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function customStringify(obj: any): string {
|
||||||
|
if (typeof obj === "undefined") {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj === null || typeof obj !== "object") {
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return `"${obj.toISOString()}"`;
|
||||||
|
}
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
const arrayItems = obj.map((item) => customStringify(item)).join(",");
|
||||||
|
return `[${arrayItems}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(obj).sort();
|
||||||
|
const keyValuePairs = keys.map(
|
||||||
|
(key) => `"${key}":${customStringify(obj[key])}`
|
||||||
|
);
|
||||||
|
return `{${keyValuePairs.join(",")}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMiddlePoint(nodes: Node[]) {
|
||||||
|
let middlePointX = 0;
|
||||||
|
let middlePointY = 0;
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
middlePointX += node.position.x;
|
||||||
|
middlePointY += node.position.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalNodes = nodes.length;
|
||||||
|
const averageX = middlePointX / totalNodes;
|
||||||
|
const averageY = middlePointY / totalNodes;
|
||||||
|
|
||||||
|
return { x: averageX, y: averageY };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateFlow(
|
||||||
|
selection: OnSelectionChangeParams,
|
||||||
|
reactFlowInstance: ReactFlowInstance,
|
||||||
|
name: string
|
||||||
|
): generateFlowType {
|
||||||
|
const newFlowData = reactFlowInstance.toObject();
|
||||||
|
|
||||||
|
/* 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) &&
|
||||||
|
selection.nodes.some((node) => node.id === edge.source)
|
||||||
|
);
|
||||||
|
newFlowData.nodes = selection.nodes;
|
||||||
|
|
||||||
|
const newFlow: FlowType = {
|
||||||
|
data: newFlowData,
|
||||||
|
name: name,
|
||||||
|
description: "",
|
||||||
|
//generating local id instead of using the id from the server, can change in the future
|
||||||
|
id: uid(),
|
||||||
|
};
|
||||||
|
// filter edges that are not connected to selected nodes on both ends
|
||||||
|
// using O(n²) aproach because the number of edges is small
|
||||||
|
// in the future we can use a better aproach using a set
|
||||||
|
return {
|
||||||
|
newFlow,
|
||||||
|
removedEdges: selection.edges.filter(
|
||||||
|
(edge) => !newFlowData.edges.includes(edge)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterFlow(
|
||||||
|
selection: OnSelectionChangeParams,
|
||||||
|
reactFlowInstance: ReactFlowInstance
|
||||||
|
) {
|
||||||
|
reactFlowInstance.setNodes((nodes) =>
|
||||||
|
nodes.filter((node) => !selection.nodes.includes(node))
|
||||||
|
);
|
||||||
|
reactFlowInstance.setEdges((edges) =>
|
||||||
|
edges.filter((edge) => !selection.edges.includes(edge))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findLastNode({ nodes, edges }: findLastNodeType) {
|
||||||
|
/*
|
||||||
|
this function receives a flow and return the last node
|
||||||
|
*/
|
||||||
|
let lastNode = nodes.find((n) => !edges.some((e) => e.source === n.id));
|
||||||
|
return lastNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
|
||||||
|
const middlePoint = getMiddlePoint(flow.data!.nodes);
|
||||||
|
let deltaPosition = {
|
||||||
|
x: NewPosition.x - middlePoint.x,
|
||||||
|
y: NewPosition.y - middlePoint.y,
|
||||||
|
};
|
||||||
|
flow.data!.nodes.forEach((node) => {
|
||||||
|
node.position.x += deltaPosition.x;
|
||||||
|
node.position.y += deltaPosition.y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function concatFlows(
|
||||||
|
flow: FlowType,
|
||||||
|
ReactFlowInstance: ReactFlowInstance
|
||||||
|
) {
|
||||||
|
const { nodes, edges } = flow.data!;
|
||||||
|
ReactFlowInstance.addNodes(nodes);
|
||||||
|
ReactFlowInstance.addEdges(edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateSelection(
|
||||||
|
selection: OnSelectionChangeParams,
|
||||||
|
edges: Edge[]
|
||||||
|
): Array<string> {
|
||||||
|
//add edges to selection if selection mode selected only nodes
|
||||||
|
if (selection.edges.length === 0) {
|
||||||
|
selection.edges = edges;
|
||||||
|
}
|
||||||
|
// get only edges that are connected to the nodes in the selection
|
||||||
|
// first creates a set of all the nodes ids
|
||||||
|
let nodesSet = new Set(selection.nodes.map((n) => n.id));
|
||||||
|
// then filter the edges that are connected to the nodes in the set
|
||||||
|
let connectedEdges = selection.edges.filter(
|
||||||
|
(e) => nodesSet.has(e.source) && nodesSet.has(e.target)
|
||||||
|
);
|
||||||
|
// add the edges to the selection
|
||||||
|
selection.edges = connectedEdges;
|
||||||
|
|
||||||
|
let errorsArray: Array<string> = [];
|
||||||
|
// check if there is more than one node
|
||||||
|
if (selection.nodes.length < 2) {
|
||||||
|
errorsArray.push("Please select more than one node");
|
||||||
|
}
|
||||||
|
|
||||||
|
//check if there are two or more nodes with free outputs
|
||||||
|
if (
|
||||||
|
selection.nodes.filter(
|
||||||
|
(n) => !selection.edges.some((e) => e.source === n.id)
|
||||||
|
).length > 1
|
||||||
|
) {
|
||||||
|
errorsArray.push("Please select only one node with free outputs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is any node that does not have any connection
|
||||||
|
if (
|
||||||
|
selection.nodes.some(
|
||||||
|
(node) =>
|
||||||
|
!selection.edges.some((edge) => edge.target === node.id) &&
|
||||||
|
!selection.edges.some((edge) => edge.source === node.id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
errorsArray.push("Please select only nodes that are connected");
|
||||||
|
}
|
||||||
|
return errorsArray;
|
||||||
|
}
|
||||||
|
function updateGroupNodeTemplate(template: APITemplateType) {
|
||||||
|
/*this function receives a template, iterates for it's items
|
||||||
|
updating the visibility of all basic types setting it to advanced true*/
|
||||||
|
Object.keys(template).forEach((key) => {
|
||||||
|
let type = template[key].type;
|
||||||
|
let input_types = template[key].input_types;
|
||||||
|
if (
|
||||||
|
(type === "str" ||
|
||||||
|
type === "bool" ||
|
||||||
|
type === "float" ||
|
||||||
|
type === "code" ||
|
||||||
|
type === "prompt" ||
|
||||||
|
type === "file" ||
|
||||||
|
type === "int" ||
|
||||||
|
type === "dict" ||
|
||||||
|
type === "NestedDict") &&
|
||||||
|
!template[key].required &&
|
||||||
|
!input_types
|
||||||
|
) {
|
||||||
|
template[key].advanced = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
export function mergeNodeTemplates({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
}: {
|
||||||
|
nodes: NodeType[];
|
||||||
|
edges: Edge[];
|
||||||
|
}): APITemplateType {
|
||||||
|
/* this function receives a flow and iterate throw each node
|
||||||
|
and merge the templates with only the visible fields
|
||||||
|
if there are two keys with the same name in the flow, we will update the display name of each one
|
||||||
|
to show from which node it came from
|
||||||
|
*/
|
||||||
|
let template: APITemplateType = {};
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
let nodeTemplate = _.cloneDeep(node.data.node!.template);
|
||||||
|
Object.keys(nodeTemplate)
|
||||||
|
.filter((field_name) => field_name.charAt(0) !== "_")
|
||||||
|
.forEach((key) => {
|
||||||
|
if (
|
||||||
|
nodeTemplate[key].show &&
|
||||||
|
!isHandleConnected(edges, key, nodeTemplate[key], node.id)
|
||||||
|
) {
|
||||||
|
template[key + "_" + node.id] = nodeTemplate[key];
|
||||||
|
template[key + "_" + node.id].proxy = { id: node.id, field: key };
|
||||||
|
if (node.type === "groupNode") {
|
||||||
|
template[key + "_" + node.id].display_name =
|
||||||
|
node.data.node!.flow!.name + " - " + nodeTemplate[key].name;
|
||||||
|
} else {
|
||||||
|
template[key + "_" + node.id].display_name =
|
||||||
|
//data id already has the node name on it
|
||||||
|
nodeTemplate[key].display_name
|
||||||
|
? nodeTemplate[key].display_name
|
||||||
|
: nodeTemplate[key].name
|
||||||
|
? toTitleCase(nodeTemplate[key].name)
|
||||||
|
: toTitleCase(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
function isHandleConnected(
|
||||||
|
edges: Edge[],
|
||||||
|
key: string,
|
||||||
|
field: TemplateVariableType,
|
||||||
|
nodeId: string
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
this function receives a flow and a handleId and check if there is a connection with this handle
|
||||||
|
*/
|
||||||
|
scapedJSONStringfy({ type: field.type, fieldName: key, id: nodeId });
|
||||||
|
if (field.proxy) {
|
||||||
|
if (
|
||||||
|
edges.some(
|
||||||
|
(e) =>
|
||||||
|
e.targetHandle ===
|
||||||
|
scapedJSONStringfy({
|
||||||
|
type: field.type,
|
||||||
|
fieldName: key,
|
||||||
|
id: nodeId,
|
||||||
|
proxy: { id: field.proxy!.id, field: field.proxy!.field },
|
||||||
|
inputTypes: field.input_types,
|
||||||
|
} as targetHandleType)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
edges.some(
|
||||||
|
(e) =>
|
||||||
|
e.targetHandle ===
|
||||||
|
scapedJSONStringfy({
|
||||||
|
type: field.type,
|
||||||
|
fieldName: key,
|
||||||
|
id: nodeId,
|
||||||
|
inputTypes: field.input_types,
|
||||||
|
} as targetHandleType)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateNodeTemplate(Flow: FlowType) {
|
||||||
|
/*
|
||||||
|
this function receives a flow and generate a template for the group node
|
||||||
|
*/
|
||||||
|
let template = mergeNodeTemplates({
|
||||||
|
nodes: Flow.data!.nodes,
|
||||||
|
edges: Flow.data!.edges,
|
||||||
|
});
|
||||||
|
updateGroupNodeTemplate(template);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateNodeFromFlow(
|
||||||
|
flow: FlowType,
|
||||||
|
getNodeId: (type: string) => string
|
||||||
|
): NodeType {
|
||||||
|
const { nodes } = flow.data!;
|
||||||
|
const outputNode = _.cloneDeep(findLastNode(flow.data!));
|
||||||
|
const position = getMiddlePoint(nodes);
|
||||||
|
let data = _.cloneDeep(flow);
|
||||||
|
const id = getNodeId(outputNode?.data.type!);
|
||||||
|
const newGroupNode: NodeType = {
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
type: outputNode?.data.type!,
|
||||||
|
node: {
|
||||||
|
output_types: outputNode!.data.node!.output_types,
|
||||||
|
display_name: "group Component",
|
||||||
|
documentation: "",
|
||||||
|
base_classes: outputNode!.data.node!.base_classes,
|
||||||
|
description: "double click to edit description",
|
||||||
|
template: generateNodeTemplate(data),
|
||||||
|
flow: data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
position,
|
||||||
|
type: "genericNode",
|
||||||
|
};
|
||||||
|
return newGroupNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectedInputNodesOnHandle(
|
||||||
|
nodeId: string,
|
||||||
|
handleId: string,
|
||||||
|
{ nodes, edges }: { nodes: NodeType[]; edges: Edge[] }
|
||||||
|
) {
|
||||||
|
const connectedNodes: Array<{ name: string; id: string; isGroup: boolean }> =
|
||||||
|
[];
|
||||||
|
// return the nodes connected to the input handle of the node
|
||||||
|
const TargetEdges = edges.filter((e) => e.target === nodeId);
|
||||||
|
TargetEdges.forEach((edge) => {
|
||||||
|
if (edge.targetHandle === handleId) {
|
||||||
|
const sourceNode = nodes.find((n) => n.id === edge.source);
|
||||||
|
if (sourceNode) {
|
||||||
|
if (sourceNode.type === "groupNode") {
|
||||||
|
let lastNode = findLastNode(sourceNode.data.node!.flow!.data!);
|
||||||
|
while (lastNode && lastNode.type === "groupNode") {
|
||||||
|
lastNode = findLastNode(lastNode.data.node!.flow!.data!);
|
||||||
|
}
|
||||||
|
if (lastNode) {
|
||||||
|
connectedNodes.push({
|
||||||
|
name: sourceNode.data.node!.flow!.name,
|
||||||
|
id: lastNode.id,
|
||||||
|
isGroup: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectedNodes.push({
|
||||||
|
name: sourceNode.data.type,
|
||||||
|
id: sourceNode.id,
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return connectedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ungroupNode(
|
||||||
|
groupNode: NodeDataType,
|
||||||
|
BaseFlow: ReactFlowJsonObject
|
||||||
|
) {
|
||||||
|
const { template, flow } = groupNode.node!;
|
||||||
|
const gNodes: NodeType[] = flow!.data!.nodes;
|
||||||
|
const gEdges = flow!.data!.edges;
|
||||||
|
//redirect edges to correct proxy node
|
||||||
|
let updatedEdges: Edge[] = [];
|
||||||
|
BaseFlow.edges.forEach((edge) => {
|
||||||
|
let newEdge = _.cloneDeep(edge);
|
||||||
|
if (newEdge.target === groupNode.id) {
|
||||||
|
const targetHandle: targetHandleType = newEdge.data.targetHandle;
|
||||||
|
if (targetHandle.proxy) {
|
||||||
|
let type = targetHandle.type;
|
||||||
|
let field = targetHandle.proxy.field;
|
||||||
|
let proxyId = targetHandle.proxy.id;
|
||||||
|
let inputTypes = targetHandle.inputTypes;
|
||||||
|
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
|
||||||
|
if (node) {
|
||||||
|
newEdge.target = proxyId;
|
||||||
|
let newTargetHandle: targetHandleType = {
|
||||||
|
fieldName: field,
|
||||||
|
type,
|
||||||
|
id: proxyId,
|
||||||
|
inputTypes: inputTypes,
|
||||||
|
};
|
||||||
|
if (node.data.node?.flow) {
|
||||||
|
newTargetHandle.proxy = {
|
||||||
|
field: node.data.node.template[field].proxy?.field!,
|
||||||
|
id: node.data.node.template[field].proxy?.id!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
newEdge.data.targetHandle = newTargetHandle;
|
||||||
|
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newEdge.source === groupNode.id) {
|
||||||
|
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
|
||||||
|
newEdge.source = lastNode!.id;
|
||||||
|
let newSourceHandle: sourceHandleType = scapeJSONParse(
|
||||||
|
newEdge.sourceHandle!
|
||||||
|
);
|
||||||
|
newSourceHandle.id = lastNode!.id;
|
||||||
|
newEdge.data.sourceHandle = newSourceHandle;
|
||||||
|
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||||
|
}
|
||||||
|
if (edge.target === groupNode.id || edge.source === groupNode.id) {
|
||||||
|
updatedEdges.push(newEdge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//update template values
|
||||||
|
Object.keys(template).forEach((key) => {
|
||||||
|
let { field, id } = template[key].proxy!;
|
||||||
|
let nodeIndex = gNodes.findIndex((n) => n.id === id);
|
||||||
|
if (nodeIndex !== -1) {
|
||||||
|
let display_name: string;
|
||||||
|
let show = gNodes[nodeIndex].data.node!.template[field].show;
|
||||||
|
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
|
||||||
|
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
|
||||||
|
display_name =
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].display_name;
|
||||||
|
} else {
|
||||||
|
display_name = gNodes[nodeIndex].data.node!.template[field].name;
|
||||||
|
}
|
||||||
|
gNodes[nodeIndex].data.node!.template[field] = template[key];
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].show = show;
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
...BaseFlow.nodes.filter((n) => n.id !== groupNode.id),
|
||||||
|
...gNodes,
|
||||||
|
];
|
||||||
|
const edges = [
|
||||||
|
...BaseFlow.edges.filter(
|
||||||
|
(e) => e.target !== groupNode.id && e.source !== groupNode.id
|
||||||
|
),
|
||||||
|
...gEdges,
|
||||||
|
...updatedEdges,
|
||||||
|
];
|
||||||
|
BaseFlow.nodes = nodes;
|
||||||
|
BaseFlow.edges = edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandGroupNode(
|
||||||
|
groupNode: NodeDataType,
|
||||||
|
ReactFlowInstance: ReactFlowInstance
|
||||||
|
) {
|
||||||
|
const { template, flow } = _.cloneDeep(groupNode.node!);
|
||||||
|
const gNodes: NodeType[] = flow?.data?.nodes!;
|
||||||
|
const gEdges = flow!.data!.edges;
|
||||||
|
console.log(gEdges);
|
||||||
|
//redirect edges to correct proxy node
|
||||||
|
let updatedEdges: Edge[] = [];
|
||||||
|
ReactFlowInstance.getEdges().forEach((edge) => {
|
||||||
|
let newEdge = _.cloneDeep(edge);
|
||||||
|
if (newEdge.target === groupNode.id) {
|
||||||
|
const targetHandle: targetHandleType = newEdge.data.targetHandle;
|
||||||
|
if (targetHandle.proxy) {
|
||||||
|
let type = targetHandle.type;
|
||||||
|
let field = targetHandle.proxy.field;
|
||||||
|
let proxyId = targetHandle.proxy.id;
|
||||||
|
let inputTypes = targetHandle.inputTypes;
|
||||||
|
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
|
||||||
|
if (node) {
|
||||||
|
newEdge.target = proxyId;
|
||||||
|
let newTargetHandle: targetHandleType = {
|
||||||
|
fieldName: field,
|
||||||
|
type,
|
||||||
|
id: proxyId,
|
||||||
|
inputTypes: inputTypes,
|
||||||
|
};
|
||||||
|
if (node.data.node?.flow) {
|
||||||
|
newTargetHandle.proxy = {
|
||||||
|
field: node.data.node.template[field].proxy?.field!,
|
||||||
|
id: node.data.node.template[field].proxy?.id!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
newEdge.data.targetHandle = newTargetHandle;
|
||||||
|
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newEdge.source === groupNode.id) {
|
||||||
|
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
|
||||||
|
newEdge.source = lastNode!.id;
|
||||||
|
let newSourceHandle: sourceHandleType = scapeJSONParse(
|
||||||
|
newEdge.sourceHandle!
|
||||||
|
);
|
||||||
|
newSourceHandle.id = lastNode!.id;
|
||||||
|
newEdge.data.sourceHandle = newSourceHandle;
|
||||||
|
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||||
|
}
|
||||||
|
if (edge.target === groupNode.id || edge.source === groupNode.id) {
|
||||||
|
updatedEdges.push(newEdge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//update template values
|
||||||
|
Object.keys(template).forEach((key) => {
|
||||||
|
let { field, id } = template[key].proxy!;
|
||||||
|
let nodeIndex = gNodes.findIndex((n) => n.id === id);
|
||||||
|
if (nodeIndex !== -1) {
|
||||||
|
let proxy: { id: string; field: string } | undefined;
|
||||||
|
let display_name: string;
|
||||||
|
let show = gNodes[nodeIndex].data.node!.template[field].show;
|
||||||
|
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
|
||||||
|
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
|
||||||
|
display_name =
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].display_name;
|
||||||
|
} else {
|
||||||
|
display_name = gNodes[nodeIndex].data.node!.template[field].name;
|
||||||
|
}
|
||||||
|
if (gNodes[nodeIndex].data.node!.template[field].proxy) {
|
||||||
|
proxy = gNodes[nodeIndex].data.node!.template[field].proxy;
|
||||||
|
}
|
||||||
|
gNodes[nodeIndex].data.node!.template[field] = template[key];
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].show = show;
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
|
||||||
|
gNodes[nodeIndex].selected = false;
|
||||||
|
if (proxy) {
|
||||||
|
gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
|
||||||
|
} else {
|
||||||
|
delete gNodes[nodeIndex].data.node!.template[field].proxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
...ReactFlowInstance.getNodes().filter((n) => n.id !== groupNode.id),
|
||||||
|
...gNodes,
|
||||||
|
];
|
||||||
|
const edges = [
|
||||||
|
...ReactFlowInstance.getEdges().filter(
|
||||||
|
(e) => e.target !== groupNode.id && e.source !== groupNode.id
|
||||||
|
),
|
||||||
|
...gEdges,
|
||||||
|
...updatedEdges,
|
||||||
|
];
|
||||||
|
console.log(edges);
|
||||||
|
ReactFlowInstance.setNodes(nodes);
|
||||||
|
ReactFlowInstance.setEdges(edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processFlow(FlowObject: ReactFlowJsonObject) {
|
||||||
|
let clonedFLow = _.cloneDeep(FlowObject);
|
||||||
|
clonedFLow.nodes.forEach((node: NodeType) => {
|
||||||
|
if (node.data.node?.flow) {
|
||||||
|
processFlow(node.data.node!.flow!.data!);
|
||||||
|
ungroupNode(node.data, clonedFLow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return clonedFLow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGroupStatus(
|
||||||
|
flow: FlowType,
|
||||||
|
ssData: { [key: string]: { valid: boolean; params: string } }
|
||||||
|
) {
|
||||||
|
let status = { valid: true, params: "Built sucessfully ✨" };
|
||||||
|
const { nodes } = flow.data!;
|
||||||
|
const ids = nodes.map((n: NodeType) => n.data.id);
|
||||||
|
ids.forEach((id) => console.log(ssData[id]));
|
||||||
|
ids.forEach((id) => {
|
||||||
|
if (!ssData[id]) {
|
||||||
|
status = ssData[id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ssData[id].valid) {
|
||||||
|
status = { valid: false, params: ssData[id].params };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
Gift,
|
Gift,
|
||||||
GitFork,
|
GitFork,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
|
Group,
|
||||||
Hammer,
|
Hammer,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Home,
|
Home,
|
||||||
|
|
@ -70,6 +71,7 @@ import {
|
||||||
TerminalSquare,
|
TerminalSquare,
|
||||||
Trash2,
|
Trash2,
|
||||||
Undo,
|
Undo,
|
||||||
|
Ungroup,
|
||||||
Unplug,
|
Unplug,
|
||||||
Upload,
|
Upload,
|
||||||
User,
|
User,
|
||||||
|
|
@ -312,8 +314,10 @@ export const nodeIconsLucide: iconsType = {
|
||||||
UserCog2,
|
UserCog2,
|
||||||
Key,
|
Key,
|
||||||
Unplug,
|
Unplug,
|
||||||
BookMarked,
|
Group,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
Ungroup,
|
||||||
|
BookMarked,
|
||||||
Minus,
|
Minus,
|
||||||
Square,
|
Square,
|
||||||
Minimize2,
|
Minimize2,
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,29 @@ chat_input_field: Input key that you want the chat to send the user message with
|
||||||
></langflow-chat>`;
|
></langflow-chat>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function truncateLongId(id: string): string {
|
||||||
|
let [componentName, newId] = id.split("-");
|
||||||
|
if (componentName.length > 15) {
|
||||||
|
componentName = componentName.slice(0, 15);
|
||||||
|
componentName += "...";
|
||||||
|
return componentName + "-" + newId;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractIdFromLongId(id: string): string {
|
||||||
|
let [_, newId] = id.split("-");
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncateDisplayName(name: string): string {
|
||||||
|
if (name.length > 15) {
|
||||||
|
name = name.slice(0, 15);
|
||||||
|
name += "...";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
export function tabsArray(codes: string[], method: number) {
|
export function tabsArray(codes: string[], method: number) {
|
||||||
if (!method) return;
|
if (!method) return;
|
||||||
if (method === 0) {
|
if (method === 0) {
|
||||||
|
|
|
||||||
100
src/frontend/tests/end-to-end/groupNode.spec.ts
Normal file
100
src/frontend/tests/end-to-end/groupNode.spec.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Group component tests", () => {
|
||||||
|
test("group test", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
await page.getByRole("button", { name: "Community Examples" }).click();
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"div:nth-child(7) > div:nth-child(2) > .card-component-footer-arrangement > .inline-flex"
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[1]/div/div[1]/div"
|
||||||
|
)
|
||||||
|
.click({
|
||||||
|
modifiers: ["Control"],
|
||||||
|
});
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[2]/div/div[1]/div"
|
||||||
|
)
|
||||||
|
.click({
|
||||||
|
modifiers: ["Control"],
|
||||||
|
});
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[3]/div/div[1]/div"
|
||||||
|
)
|
||||||
|
.click({
|
||||||
|
modifiers: ["Control"],
|
||||||
|
});
|
||||||
|
await page.getByRole("button", { name: "Group" }).click();
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
|
||||||
|
)
|
||||||
|
.isVisible()
|
||||||
|
).toBeTruthy();
|
||||||
|
await page.getByPlaceholder("Type something...").first().click();
|
||||||
|
await page.getByPlaceholder("Type something...").first().fill("test");
|
||||||
|
await page.locator(".side-bar-buttons-arrangement").click();
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div/div/div[1]/div/div[1]/div/div"
|
||||||
|
)
|
||||||
|
.textContent()
|
||||||
|
).toBe("test");
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
|
||||||
|
)
|
||||||
|
.locator('input[type="text"]')
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
|
||||||
|
)
|
||||||
|
.locator('input[type="text"]')
|
||||||
|
.fill("fieldValue");
|
||||||
|
await page.locator(".side-bar-buttons-arrangement").click();
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div"
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
await page.getByLabel("Edit").click();
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "zero-shot-react-description" })
|
||||||
|
.click();
|
||||||
|
await page.getByText("openai-functions").click();
|
||||||
|
await page.getByRole("button", { name: "Save Changes" }).click();
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
await page.getByLabel("Ungroup").click();
|
||||||
|
await expect(
|
||||||
|
page.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[3]/div/div[2]/div[4]/div/div[2]/div/input"
|
||||||
|
)
|
||||||
|
).toHaveValue("fieldValue");
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[2]/div/div[2]/div[5]/div/div[2]/div/button/span[1]"
|
||||||
|
)
|
||||||
|
.textContent()
|
||||||
|
).toBe("openai-functions");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -35,6 +35,16 @@ def pytest_configure():
|
||||||
pytest.OPENAPI_EXAMPLE_PATH = (
|
pytest.OPENAPI_EXAMPLE_PATH = (
|
||||||
Path(__file__).parent.absolute() / "data" / "Openapi.json"
|
Path(__file__).parent.absolute() / "data" / "Openapi.json"
|
||||||
)
|
)
|
||||||
|
pytest.GROUPED_CHAT_EXAMPLE_PATH = (
|
||||||
|
Path(__file__).parent.absolute() / "data" / "grouped_chat.json"
|
||||||
|
)
|
||||||
|
pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = (
|
||||||
|
Path(__file__).parent.absolute() / "data" / "one_group_chat.json"
|
||||||
|
)
|
||||||
|
pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH = (
|
||||||
|
Path(__file__).parent.absolute() / "data" / "vector_store_grouped.json"
|
||||||
|
)
|
||||||
|
|
||||||
pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = (
|
pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = (
|
||||||
Path(__file__).parent.absolute() / "data" / "BasicChatwithPromptandHistory.json"
|
Path(__file__).parent.absolute() / "data" / "BasicChatwithPromptandHistory.json"
|
||||||
)
|
)
|
||||||
|
|
@ -159,6 +169,24 @@ def json_flow():
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def grouped_chat_json_flow():
|
||||||
|
with open(pytest.GROUPED_CHAT_EXAMPLE_PATH, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def one_grouped_chat_json_flow():
|
||||||
|
with open(pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def vector_store_grouped_json_flow():
|
||||||
|
with open(pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def json_flow_with_prompt_and_history():
|
def json_flow_with_prompt_and_history():
|
||||||
with open(pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY, "r") as f:
|
with open(pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY, "r") as f:
|
||||||
|
|
|
||||||
1
tests/data/grouped_chat.json
Normal file
1
tests/data/grouped_chat.json
Normal file
File diff suppressed because one or more lines are too long
1302
tests/data/one_group_chat.json
Normal file
1302
tests/data/one_group_chat.json
Normal file
File diff suppressed because it is too large
Load diff
1
tests/data/vector_store_grouped.json
Normal file
1
tests/data/vector_store_grouped.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,4 @@
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -17,6 +18,15 @@ from langflow.graph.vertex.types import (
|
||||||
)
|
)
|
||||||
from langflow.processing.process import get_result_and_thought
|
from langflow.processing.process import get_result_and_thought
|
||||||
from langflow.utils.payload import get_root_node
|
from langflow.utils.payload import get_root_node
|
||||||
|
from langflow.graph.graph.utils import (
|
||||||
|
find_last_node,
|
||||||
|
set_new_target_handle,
|
||||||
|
ungroup_node,
|
||||||
|
process_flow,
|
||||||
|
update_source_handle,
|
||||||
|
update_target_handle,
|
||||||
|
update_template,
|
||||||
|
)
|
||||||
|
|
||||||
# Test cases for the graph module
|
# Test cases for the graph module
|
||||||
|
|
||||||
|
|
@ -24,6 +34,52 @@ from langflow.utils.payload import get_root_node
|
||||||
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
|
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_template():
|
||||||
|
return {
|
||||||
|
"field1": {"proxy": {"field": "some_field", "id": "node1"}},
|
||||||
|
"field2": {"proxy": {"field": "other_field", "id": "node2"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_nodes():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": "node1",
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"template": {
|
||||||
|
"some_field": {"show": True, "advanced": False, "name": "Name1"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node2",
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"template": {
|
||||||
|
"other_field": {
|
||||||
|
"show": False,
|
||||||
|
"advanced": True,
|
||||||
|
"display_name": "DisplayName2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node3",
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"template": {"unrelated_field": {"show": True, "advanced": True}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_node_by_type(graph, node_type: Type[Vertex]) -> Union[Vertex, None]:
|
def get_node_by_type(graph, node_type: Type[Vertex]) -> Union[Vertex, None]:
|
||||||
"""Get a node by type"""
|
"""Get a node by type"""
|
||||||
return next((node for node in graph.nodes if isinstance(node, node_type)), None)
|
return next((node for node in graph.nodes if isinstance(node, node_type)), None)
|
||||||
|
|
@ -113,55 +169,6 @@ def test_get_node_neighbors_basic(basic_graph):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# def test_get_node_neighbors_complex(complex_graph):
|
|
||||||
# """Test getting node neighbors"""
|
|
||||||
# assert isinstance(complex_graph, Graph)
|
|
||||||
# # Get root node
|
|
||||||
# root = get_root_node(complex_graph)
|
|
||||||
# assert root is not None
|
|
||||||
# neighbors = complex_graph.get_nodes_with_target(root)
|
|
||||||
# assert neighbors is not None
|
|
||||||
# # Neighbors should be a list of nodes
|
|
||||||
# assert isinstance(neighbors, list)
|
|
||||||
# # Root Node is an Agent, it requires an LLMChain and tools
|
|
||||||
# # We need to check if there is a Chain in the one of the neighbors'
|
|
||||||
# assert any("Chain" in neighbor.data["type"] for neighbor in neighbors)
|
|
||||||
# # assert Tool is in the neighbors
|
|
||||||
# assert any("Tool" in neighbor.data["type"] for neighbor in neighbors)
|
|
||||||
# # Now on to the Chain's neighbors
|
|
||||||
# chain = next(neighbor for neighbor in neighbors if "Chain" in neighbor.data["type"])
|
|
||||||
# chain_neighbors = complex_graph.get_nodes_with_target(chain)
|
|
||||||
# assert chain_neighbors is not None
|
|
||||||
# # Check if there is a LLM in the chain's neighbors
|
|
||||||
# assert any("OpenAI" in neighbor.data["type"] for neighbor in chain_neighbors)
|
|
||||||
# # Chain should have a Prompt as a neighbor
|
|
||||||
# assert any("Prompt" in neighbor.data["type"] for neighbor in chain_neighbors)
|
|
||||||
# # Now on to the Tool's neighbors
|
|
||||||
# tool = next(neighbor for neighbor in neighbors if "Tool" in neighbor.data["type"])
|
|
||||||
# tool_neighbors = complex_graph.get_nodes_with_target(tool)
|
|
||||||
# assert tool_neighbors is not None
|
|
||||||
# # Check if there is an Agent in the tool's neighbors
|
|
||||||
# assert any("Agent" in neighbor.data["type"] for neighbor in tool_neighbors)
|
|
||||||
# # This Agent has a Tool that has a PythonFunction as func
|
|
||||||
# agent = next(
|
|
||||||
# neighbor for neighbor in tool_neighbors if "Agent" in neighbor.data["type"]
|
|
||||||
# )
|
|
||||||
# agent_neighbors = complex_graph.get_nodes_with_target(agent)
|
|
||||||
# assert agent_neighbors is not None
|
|
||||||
# # Check if there is a Tool in the agent's neighbors
|
|
||||||
# assert any("Tool" in neighbor.data["type"] for neighbor in agent_neighbors)
|
|
||||||
# # This Tool has a PythonFunction as func
|
|
||||||
# tool = next(
|
|
||||||
# neighbor for neighbor in agent_neighbors if "Tool" in neighbor.data["type"]
|
|
||||||
# )
|
|
||||||
# tool_neighbors = complex_graph.get_nodes_with_target(tool)
|
|
||||||
# assert tool_neighbors is not None
|
|
||||||
# # Check if there is a PythonFunction in the tool's neighbors
|
|
||||||
# assert any(
|
|
||||||
# "PythonFunctionTool" in neighbor.data["type"] for neighbor in tool_neighbors
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_node(basic_graph):
|
def test_get_node(basic_graph):
|
||||||
"""Test getting a single node"""
|
"""Test getting a single node"""
|
||||||
node_id = basic_graph.nodes[0].id
|
node_id = basic_graph.nodes[0].id
|
||||||
|
|
@ -322,6 +329,196 @@ def test_get_result_and_thought(basic_graph):
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_last_node(grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||||
|
nodes, edges = grouped_chat_data["nodes"], grouped_chat_data["edges"]
|
||||||
|
last_node = find_last_node(nodes, edges)
|
||||||
|
assert last_node is not None # Replace with the actual expected value
|
||||||
|
assert last_node["id"] == "LLMChain-pimAb" # Replace with the actual expected value
|
||||||
|
|
||||||
|
|
||||||
|
def test_ungroup_node(grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||||
|
group_node = grouped_chat_data["nodes"][
|
||||||
|
2
|
||||||
|
] # Assuming the first node is a group node
|
||||||
|
base_flow = copy.deepcopy(grouped_chat_data)
|
||||||
|
ungroup_node(group_node["data"], base_flow)
|
||||||
|
# after ungroup_node is called, the base_flow and grouped_chat_data should be different
|
||||||
|
assert base_flow != grouped_chat_data
|
||||||
|
# assert node 2 is not a group node anymore
|
||||||
|
assert base_flow["nodes"][2]["data"]["node"].get("flow") is None
|
||||||
|
# assert the edges are updated
|
||||||
|
assert len(base_flow["edges"]) > len(grouped_chat_data["edges"])
|
||||||
|
assert base_flow["edges"][0]["source"] == "ConversationBufferMemory-kUMif"
|
||||||
|
assert base_flow["edges"][0]["target"] == "LLMChain-2P369"
|
||||||
|
assert base_flow["edges"][1]["source"] == "PromptTemplate-Wjk4g"
|
||||||
|
assert base_flow["edges"][1]["target"] == "LLMChain-2P369"
|
||||||
|
assert base_flow["edges"][2]["source"] == "ChatOpenAI-rUJ1b"
|
||||||
|
assert base_flow["edges"][2]["target"] == "LLMChain-2P369"
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_flow(grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||||
|
|
||||||
|
processed_flow = process_flow(grouped_chat_data)
|
||||||
|
assert processed_flow is not None
|
||||||
|
assert isinstance(processed_flow, dict)
|
||||||
|
assert "nodes" in processed_flow
|
||||||
|
assert "edges" in processed_flow
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_flow_one_group(one_grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(one_grouped_chat_json_flow).get("data")
|
||||||
|
# There should be only one node
|
||||||
|
assert len(grouped_chat_data["nodes"]) == 1
|
||||||
|
# Get the node, it should be a group node
|
||||||
|
group_node = grouped_chat_data["nodes"][0]
|
||||||
|
node_data = group_node["data"]["node"]
|
||||||
|
assert node_data.get("flow") is not None
|
||||||
|
template_data = node_data["template"]
|
||||||
|
assert any("openai_api_key" in key for key in template_data.keys())
|
||||||
|
# Get the openai_api_key dict
|
||||||
|
openai_api_key = next(
|
||||||
|
(template_data[key] for key in template_data.keys() if "openai_api_key" in key),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert openai_api_key is not None
|
||||||
|
assert openai_api_key["value"] == "test"
|
||||||
|
|
||||||
|
processed_flow = process_flow(grouped_chat_data)
|
||||||
|
assert processed_flow is not None
|
||||||
|
assert isinstance(processed_flow, dict)
|
||||||
|
assert "nodes" in processed_flow
|
||||||
|
assert "edges" in processed_flow
|
||||||
|
|
||||||
|
# Now get the node that has ChatOpenAI in its id
|
||||||
|
chat_openai_node = next(
|
||||||
|
(node for node in processed_flow["nodes"] if "ChatOpenAI" in node["id"]), None
|
||||||
|
)
|
||||||
|
assert chat_openai_node is not None
|
||||||
|
assert (
|
||||||
|
chat_openai_node["data"]["node"]["template"]["openai_api_key"]["value"]
|
||||||
|
== "test"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_flow_vector_store_grouped(vector_store_grouped_json_flow):
|
||||||
|
grouped_chat_data = json.loads(vector_store_grouped_json_flow).get("data")
|
||||||
|
nodes = grouped_chat_data["nodes"]
|
||||||
|
assert len(nodes) == 4
|
||||||
|
# There are two group nodes in this flow
|
||||||
|
# One of them is inside the other totalling 7 nodes
|
||||||
|
# 4 nodes grouped, one of these turns into 1 normal node and 1 group node
|
||||||
|
# This group node has 2 nodes inside it
|
||||||
|
|
||||||
|
processed_flow = process_flow(grouped_chat_data)
|
||||||
|
assert processed_flow is not None
|
||||||
|
processed_nodes = processed_flow["nodes"]
|
||||||
|
assert len(processed_nodes) == 7
|
||||||
|
assert isinstance(processed_flow, dict)
|
||||||
|
assert "nodes" in processed_flow
|
||||||
|
assert "edges" in processed_flow
|
||||||
|
edges = processed_flow["edges"]
|
||||||
|
# Expected keywords in source and target fields
|
||||||
|
expected_keywords = [
|
||||||
|
{"source": "VectorStoreInfo", "target": "VectorStoreAgent"},
|
||||||
|
{"source": "ChatOpenAI", "target": "VectorStoreAgent"},
|
||||||
|
{"source": "OpenAIEmbeddings", "target": "Chroma"},
|
||||||
|
{"source": "Chroma", "target": "VectorStoreInfo"},
|
||||||
|
{"source": "WebBaseLoader", "target": "RecursiveCharacterTextSplitter"},
|
||||||
|
{"source": "RecursiveCharacterTextSplitter", "target": "Chroma"},
|
||||||
|
]
|
||||||
|
|
||||||
|
for idx, expected_keyword in enumerate(expected_keywords):
|
||||||
|
for key, value in expected_keyword.items():
|
||||||
|
assert (
|
||||||
|
value in edges[idx][key].split("-")[0]
|
||||||
|
), f"Edge {idx}, key {key} expected to contain {value} but got {edges[idx][key]}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_template(sample_template, sample_nodes):
|
||||||
|
# Making a deep copy to keep original sample_nodes unchanged
|
||||||
|
nodes_copy = copy.deepcopy(sample_nodes)
|
||||||
|
update_template(sample_template, nodes_copy)
|
||||||
|
|
||||||
|
# Now, validate the updates.
|
||||||
|
node1_updated = next((n for n in nodes_copy if n["id"] == "node1"), None)
|
||||||
|
node2_updated = next((n for n in nodes_copy if n["id"] == "node2"), None)
|
||||||
|
node3_updated = next((n for n in nodes_copy if n["id"] == "node3"), None)
|
||||||
|
|
||||||
|
assert node1_updated["data"]["node"]["template"]["some_field"]["show"] is True
|
||||||
|
assert node1_updated["data"]["node"]["template"]["some_field"]["advanced"] is False
|
||||||
|
assert (
|
||||||
|
node1_updated["data"]["node"]["template"]["some_field"]["display_name"]
|
||||||
|
== "Name1"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert node2_updated["data"]["node"]["template"]["other_field"]["show"] is False
|
||||||
|
assert node2_updated["data"]["node"]["template"]["other_field"]["advanced"] is True
|
||||||
|
assert (
|
||||||
|
node2_updated["data"]["node"]["template"]["other_field"]["display_name"]
|
||||||
|
== "DisplayName2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure node3 remains unchanged
|
||||||
|
assert node3_updated == sample_nodes[2]
|
||||||
|
|
||||||
|
|
||||||
|
# Test `update_target_handle`
|
||||||
|
def test_update_target_handle_proxy():
|
||||||
|
new_edge = {
|
||||||
|
"data": {
|
||||||
|
"targetHandle": {
|
||||||
|
"type": "some_type",
|
||||||
|
"proxy": {"id": "some_id", "field": ""},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_nodes = [{"id": "some_id", "data": {"node": {"flow": None}}}]
|
||||||
|
group_node_id = "group_id"
|
||||||
|
updated_edge = update_target_handle(new_edge, g_nodes, group_node_id)
|
||||||
|
assert updated_edge["data"]["targetHandle"] == new_edge["data"]["targetHandle"]
|
||||||
|
|
||||||
|
|
||||||
|
# Test `set_new_target_handle`
|
||||||
|
def test_set_new_target_handle():
|
||||||
|
proxy_id = "proxy_id"
|
||||||
|
new_edge = {"target": None, "data": {"targetHandle": {}}}
|
||||||
|
target_handle = {"type": "type_1", "proxy": {"field": "field_1"}}
|
||||||
|
node = {
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"flow": True,
|
||||||
|
"template": {
|
||||||
|
"field_1": {"proxy": {"field": "new_field", "id": "new_id"}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_new_target_handle(proxy_id, new_edge, target_handle, node)
|
||||||
|
assert new_edge["target"] == "proxy_id"
|
||||||
|
assert new_edge["data"]["targetHandle"]["fieldName"] == "field_1"
|
||||||
|
assert new_edge["data"]["targetHandle"]["proxy"] == {
|
||||||
|
"field": "new_field",
|
||||||
|
"id": "new_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Test `update_source_handle`
|
||||||
|
def test_update_source_handle():
|
||||||
|
new_edge = {"source": None, "data": {"sourceHandle": {"id": None}}}
|
||||||
|
flow_data = {
|
||||||
|
"nodes": [{"id": "some_node"}, {"id": "last_node"}],
|
||||||
|
"edges": [{"source": "some_node"}],
|
||||||
|
}
|
||||||
|
updated_edge = update_source_handle(
|
||||||
|
new_edge, flow_data["nodes"], flow_data["edges"]
|
||||||
|
)
|
||||||
|
assert updated_edge["source"] == "last_node"
|
||||||
|
assert updated_edge["data"]["sourceHandle"]["id"] == "last_node"
|
||||||
|
|
||||||
|
|
||||||
def test_pickle_graph(json_vector_store):
|
def test_pickle_graph(json_vector_store):
|
||||||
loaded_json = json.loads(json_vector_store)
|
loaded_json = json.loads(json_vector_store)
|
||||||
graph = Graph.from_payload(loaded_json)
|
graph = Graph.from_payload(loaded_json)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
template = prompt["template"]
|
template = prompt["template"]
|
||||||
assert template["input_variables"] == {
|
assert template["input_variables"] == {
|
||||||
"required": True,
|
"required": True,
|
||||||
"dynamic": False,
|
"dynamic": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"show": False,
|
"show": False,
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
|
|
@ -35,7 +35,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
|
|
||||||
assert template["output_parser"] == {
|
assert template["output_parser"] == {
|
||||||
"required": False,
|
"required": False,
|
||||||
"dynamic": False,
|
"dynamic": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"show": False,
|
"show": False,
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
|
|
@ -49,7 +49,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
|
|
||||||
assert template["partial_variables"] == {
|
assert template["partial_variables"] == {
|
||||||
"required": False,
|
"required": False,
|
||||||
"dynamic": False,
|
"dynamic": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"show": False,
|
"show": False,
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
|
|
@ -63,7 +63,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
|
|
||||||
assert template["template"] == {
|
assert template["template"] == {
|
||||||
"required": True,
|
"required": True,
|
||||||
"dynamic": False,
|
"dynamic": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"show": True,
|
"show": True,
|
||||||
"multiline": True,
|
"multiline": True,
|
||||||
|
|
@ -77,7 +77,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
|
|
||||||
assert template["template_format"] == {
|
assert template["template_format"] == {
|
||||||
"required": False,
|
"required": False,
|
||||||
"dynamic": False,
|
"dynamic": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"show": False,
|
"show": False,
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
|
|
@ -92,7 +92,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
|
|
||||||
assert template["validate_template"] == {
|
assert template["validate_template"] == {
|
||||||
"required": False,
|
"required": False,
|
||||||
"dynamic": False,
|
"dynamic": True,
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"show": False,
|
"show": False,
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue