Merge remote-tracking branch 'origin/dev' into bugfix_minimizeNode
This commit is contained in:
commit
6ae41c265c
67 changed files with 5049 additions and 1228 deletions
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
|
@ -4,10 +4,18 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
|
|
|
|||
9
.github/workflows/lint.yml
vendored
9
.github/workflows/lint.yml
vendored
|
|
@ -3,7 +3,16 @@ name: lint
|
|||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.4.0"
|
||||
|
|
|
|||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
|
@ -3,9 +3,17 @@ name: test
|
|||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
env:
|
||||
POETRY_VERSION: "1.5.0"
|
||||
|
||||
|
|
|
|||
12
Makefile
12
Makefile
|
|
@ -53,8 +53,8 @@ setup_devcontainer:
|
|||
poetry run langflow --path src/frontend/build
|
||||
|
||||
frontend:
|
||||
make install_frontend
|
||||
make run_frontend
|
||||
@-make install_frontend || (echo "An error occurred while installing frontend dependencies. Attempting to fix." && make install_frontendc)
|
||||
@make run_frontend
|
||||
|
||||
frontendc:
|
||||
make install_frontendc
|
||||
|
|
@ -65,7 +65,13 @@ install_backend:
|
|||
|
||||
backend:
|
||||
make install_backend
|
||||
poetry run uvicorn --factory src.backend.langflow.main:create_app --port 7860 --reload --log-level debug
|
||||
ifeq ($(login),1)
|
||||
@echo "Running backend without autologin";
|
||||
poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser
|
||||
else
|
||||
@echo "Running backend with autologin";
|
||||
LANGFLOW_AUTO_LOGIN=True poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser
|
||||
endif
|
||||
|
||||
build_and_run:
|
||||
echo 'Removing dist folder'
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
|
|||
|
||||
# 🎨 Creating Flows
|
||||
|
||||
Creating flows with Langflow is easy. Simply drag sidebar components onto the canvas and connect them together to create your pipeline. Langflow provides a range of [LangChain components](https://langchain.readthedocs.io/en/latest/reference.html) to choose from, including LLMs, prompt serializers, agents, and chains.
|
||||
Creating flows with Langflow is easy. Simply drag sidebar components onto the canvas and connect them together to create your pipeline. Langflow provides a range of [LangChain components](https://docs.langchain.com/docs/category/components) to choose from, including LLMs, prompt serializers, agents, and chains.
|
||||
|
||||
Explore by editing prompt parameters, link chains and agents, track an agent's thought process, and export your flow.
|
||||
|
||||
|
|
|
|||
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.
Binary file not shown.
|
Before Width: | Height: | Size: 2 MiB After Width: | Height: | Size: 2 MiB |
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
|
||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||
|
||||
response = {
|
||||
"valid": valid,
|
||||
"params": params,
|
||||
"id": vertex.id,
|
||||
"progress": round(i / number_of_nodes, 2),
|
||||
}
|
||||
vertex_id = (
|
||||
vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
|
||||
)
|
||||
if vertex_id in graph.top_level_nodes:
|
||||
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()
|
||||
# Now we need to check the input_keys to send them to the client
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def get_all(
|
|||
"/process/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
)
|
||||
async def process_flow(
|
||||
async def process(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[dict] = None,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,79 @@
|
|||
from loguru import logger
|
||||
from typing import TYPE_CHECKING
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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:
|
||||
def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
|
||||
self.source: "Vertex" = source
|
||||
self.target: "Vertex" = target
|
||||
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]
|
||||
|
||||
if data := edge.get("data", {}):
|
||||
self._source_handle = data.get("sourceHandle", {})
|
||||
self._target_handle = data.get("targetHandle", {})
|
||||
self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
|
||||
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()
|
||||
|
||||
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):
|
||||
self.source = state["source"]
|
||||
self.target = state["target"]
|
||||
self.target_param = state["target_param"]
|
||||
self.source_handle = state["source_handle"]
|
||||
self.target_handle = state["target_handle"]
|
||||
self.source_handle = state.get("source_handle")
|
||||
self.target_handle = state.get("target_handle")
|
||||
|
||||
def reset(self) -> None:
|
||||
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.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.types import (
|
||||
FileToolVertex,
|
||||
|
|
@ -19,11 +20,21 @@ class Graph:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]],
|
||||
nodes: List[Dict],
|
||||
edges: List[Dict[str, str]],
|
||||
) -> None:
|
||||
self._nodes = nodes
|
||||
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()
|
||||
|
||||
def __setstate__(self, state):
|
||||
|
|
@ -50,6 +61,7 @@ class Graph:
|
|||
edges = payload["edges"]
|
||||
return cls(nodes, edges)
|
||||
except KeyError as exc:
|
||||
logger.exception(exc)
|
||||
raise ValueError(
|
||||
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
|
||||
) from exc
|
||||
|
|
@ -215,7 +227,9 @@ class Graph:
|
|||
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.is_task = is_task
|
||||
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):
|
||||
for edge in self.edges:
|
||||
|
|
@ -88,6 +90,11 @@ class Vertex:
|
|||
self._built = False
|
||||
self.artifacts: Dict[str, Any] = {}
|
||||
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:
|
||||
self.data = self._data["data"]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ def initialize_database():
|
|||
try:
|
||||
database_service.run_migrations()
|
||||
except CommandError as exc:
|
||||
if "Can't locate revision identified by" not in str(exc):
|
||||
# if "overlaps with other requested revisions" or "Can't locate revision identified by"
|
||||
# are not in the exception, we can't handle it
|
||||
if "overlaps with other requested revisions" not in str(
|
||||
exc
|
||||
) and "Can't locate revision identified by" not in str(exc):
|
||||
raise exc
|
||||
# This means there's wrong revision in the DB
|
||||
# We need to delete the alembic_version table
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class PromptFrontendNode(FrontendNode):
|
|||
|
||||
# All prompt fields should be password=False
|
||||
field.password = False
|
||||
field.dynamic = True
|
||||
|
||||
|
||||
class PromptTemplateNode(FrontendNode):
|
||||
|
|
|
|||
2
src/frontend/.gitignore
vendored
2
src/frontend/.gitignore
vendored
|
|
@ -22,5 +22,5 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright-report/*/
|
||||
/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. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* 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. */
|
||||
use: {
|
||||
/* 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 */
|
||||
trace: "on-first-retry",
|
||||
|
|
@ -69,9 +71,16 @@ export default defineConfig({
|
|||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
// webServer: [
|
||||
// {
|
||||
// command: "npm run backend",
|
||||
// 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,
|
||||
hasDuplicateKeys,
|
||||
isValidConnection,
|
||||
scapedJSONStringfy,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import {
|
||||
nodeColors,
|
||||
|
|
@ -53,6 +54,7 @@ export default function ParameterComponent({
|
|||
required = false,
|
||||
optionalHandle = null,
|
||||
info = "",
|
||||
proxy,
|
||||
showNode,
|
||||
}: ParameterComponentType): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -80,8 +82,9 @@ export default function ParameterComponent({
|
|||
|
||||
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
|
||||
false;
|
||||
reactFlowInstance
|
||||
?.getEdges()
|
||||
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
|
||||
|
||||
const { data: myData } = useContext(typesContext);
|
||||
|
||||
|
|
@ -112,7 +115,6 @@ export default function ParameterComponent({
|
|||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (name === "openai_api_base") console.log(info);
|
||||
// @ts-ignore
|
||||
infoHtml.current = (
|
||||
<div className="h-full w-full break-words">
|
||||
|
|
@ -210,32 +212,40 @@ export default function ParameterComponent({
|
|||
!optionalHandle ? (
|
||||
<></>
|
||||
) : (
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||
<div className="flex">
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
|
|
@ -250,7 +260,13 @@ export default function ParameterComponent({
|
|||
(info !== "" ? " flex items-center" : "")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{proxy ? (
|
||||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||
<span>{title}</span>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
<div className="">
|
||||
{info !== "" && (
|
||||
|
|
@ -290,7 +306,11 @@ export default function ParameterComponent({
|
|||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
|
|
@ -373,6 +393,11 @@ export default function ParameterComponent({
|
|||
) : left === true && type === "code" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
data.node?.flow && data.node.template[name].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={data.node?.template[name].dynamic ?? false}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
|
|
@ -407,6 +432,11 @@ export default function ParameterComponent({
|
|||
) : left === true && type === "prompt" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
data.node?.flow && data.node.template[name].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
field_name={name}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
|
|
|
|||
|
|
@ -4,23 +4,33 @@ import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
|||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { useSSE } from "../../contexts/SSEContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { cleanEdges } from "../../utils/reactflowUtils";
|
||||
import {
|
||||
cleanEdges,
|
||||
handleKeyDown,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, toTitleCase } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
|
||||
export default function GenericNode({
|
||||
data: olddata,
|
||||
xPos,
|
||||
yPos,
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}): JSX.Element {
|
||||
const [data, setData] = useState(olddata);
|
||||
const { updateFlow, flows, tabId } = useContext(TabsContext);
|
||||
|
|
@ -28,6 +38,12 @@ export default function GenericNode({
|
|||
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
|
||||
useContext(typesContext);
|
||||
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] =
|
||||
useState<validationStatusType | null>(null);
|
||||
const [showNode, setShowNode] = useState<boolean>(true);
|
||||
|
|
@ -111,6 +127,7 @@ export default function GenericNode({
|
|||
<>
|
||||
<NodeToolbar>
|
||||
<NodeToolbarComponent
|
||||
position={{ x: xPos, y: yPos }}
|
||||
data={data}
|
||||
setData={setData}
|
||||
deleteNode={deleteNode}
|
||||
|
|
@ -151,7 +168,7 @@ export default function GenericNode({
|
|||
}
|
||||
>
|
||||
<IconComponent
|
||||
name={name}
|
||||
name={data.node?.flow ? "Ungroup" : name}
|
||||
className={
|
||||
"generic-node-icon " +
|
||||
(!showNode && "absolute inset-x-6 h-12 w-12")
|
||||
|
|
@ -160,11 +177,35 @@ export default function GenericNode({
|
|||
/>
|
||||
{showNode && (
|
||||
<div className="generic-node-tooltip-div">
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div className="generic-node-tooltip-div text-primary">
|
||||
{data.node?.display_name}
|
||||
{data.node?.flow && inputName ? (
|
||||
<div>
|
||||
<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>
|
||||
</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>
|
||||
|
|
@ -178,16 +219,14 @@ export default function GenericNode({
|
|||
data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced && (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node!.template[
|
||||
templateField
|
||||
].input_types?.join(";") ??
|
||||
data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
key={scapedJSONStringfy({
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
|
|
@ -217,31 +256,31 @@ export default function GenericNode({
|
|||
data.node?.template[templateField].type
|
||||
}
|
||||
required={
|
||||
data.node?.template[templateField].required
|
||||
}
|
||||
id={
|
||||
(data.node?.template[
|
||||
templateField
|
||||
].input_types?.join(";") ??
|
||||
data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
data.node!.template[templateField].required
|
||||
}
|
||||
id={{
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
}}
|
||||
left={true}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<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}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
|
|
@ -252,9 +291,11 @@ export default function GenericNode({
|
|||
: data.type
|
||||
}
|
||||
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("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
|
|
@ -330,16 +371,53 @@ export default function GenericNode({
|
|||
className={
|
||||
showNode
|
||||
? "generic-node-desc " +
|
||||
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
|
||||
(data.node?.description !== "" ? "py-5" : "pb-5")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{data.node?.description !== "" && showNode && (
|
||||
<div className="generic-node-desc-text">
|
||||
{data.node?.description !== "" &&
|
||||
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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<>
|
||||
{Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
|
|
@ -348,15 +426,14 @@ export default function GenericNode({
|
|||
{data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced ? (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node!.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
key={scapedJSONStringfy({
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
|
|
@ -384,21 +461,20 @@ export default function GenericNode({
|
|||
"\n"
|
||||
) ?? data.node?.template[templateField].type
|
||||
}
|
||||
required={data.node?.template[templateField].required}
|
||||
id={
|
||||
(data.node?.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
required={data.node!.template[templateField].required}
|
||||
id={{
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
}}
|
||||
left={true}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -415,7 +491,11 @@ export default function GenericNode({
|
|||
{" "}
|
||||
</div>
|
||||
<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}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
|
|
@ -425,7 +505,11 @@ export default function GenericNode({
|
|||
: data.type
|
||||
}
|
||||
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("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default function DropdownButton({
|
|||
<DropdownMenu open={showOptions}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
id="new-project-btn"
|
||||
variant="primary"
|
||||
className="relative pr-10"
|
||||
onClick={(event) => {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
type="text"
|
||||
name="name"
|
||||
value={name ?? ""}
|
||||
placeholder="File name"
|
||||
placeholder="Flow name"
|
||||
id="name"
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -80,61 +80,54 @@ export default function BuildTrigger({
|
|||
const { flowId } = response.data;
|
||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||
let validationResults: boolean[] = [];
|
||||
let finished = false;
|
||||
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) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
// Close the connection and finish
|
||||
finished = true;
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
eventSource.close();
|
||||
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();
|
||||
|
||||
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);
|
||||
reject(new Error("Streaming failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function processStreamResult(parsedData: parsedDataType) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default function CodeAreaComponent({
|
|||
nodeClass,
|
||||
dynamic,
|
||||
setNodeClass,
|
||||
readonly = false,
|
||||
}: CodeAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(
|
||||
typeof value == "string" ? value : JSON.stringify(value)
|
||||
|
|
@ -30,6 +31,7 @@ export default function CodeAreaComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<CodeAreaModal
|
||||
readonly={readonly}
|
||||
dynamic={dynamic}
|
||||
value={myValue}
|
||||
nodeClass={nodeClass}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default function CodeTabsComponent({
|
|||
}, [flow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tweaks) {
|
||||
if (tweaks && data) {
|
||||
unselectAllNodes({
|
||||
data,
|
||||
updateNodes: (nodes) => {
|
||||
|
|
@ -604,6 +604,14 @@ export default function CodeTabsComponent({
|
|||
].type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
node.data.node?.flow &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={
|
||||
|
|
@ -646,6 +654,14 @@ export default function CodeTabsComponent({
|
|||
<CodeAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
readonly={
|
||||
node.data.node?.flow &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function FloatComponent({
|
|||
return (
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id="float-input"
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
|||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow(undefined, true).then((id) => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
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 { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export default function InputComponent({
|
||||
autoFocus = false,
|
||||
onBlur,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
|
|
@ -15,9 +17,10 @@ export default function InputComponent({
|
|||
editNode = false,
|
||||
placeholder = "Type something...",
|
||||
className,
|
||||
blurOnEnter = false,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -30,6 +33,9 @@ export default function InputComponent({
|
|||
{isForm ? (
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
ref={refInput}
|
||||
onBlur={onBlur}
|
||||
autoFocus={autoFocus}
|
||||
type={password && !pwdVisible ? "password" : "text"}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
|
|
@ -49,13 +55,17 @@ export default function InputComponent({
|
|||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
) : (
|
||||
<Input
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={classNames(
|
||||
|
|
@ -73,6 +83,7 @@ export default function InputComponent({
|
|||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export default function KeypairListComponent({
|
|||
return (
|
||||
<div key={idx} className="flex w-full gap-2">
|
||||
<Input
|
||||
id={"keypair" + index}
|
||||
type="text"
|
||||
value={key.trim()}
|
||||
className={classNames(
|
||||
|
|
@ -72,6 +73,7 @@ export default function KeypairListComponent({
|
|||
/>
|
||||
|
||||
<Input
|
||||
id={"keypair" + (index + 100).toString()}
|
||||
type="text"
|
||||
value={obj[key]}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
|
|
@ -88,6 +90,7 @@ export default function KeypairListComponent({
|
|||
newInputList.push({ "": "" });
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={"plusbtn" + index.toString()}
|
||||
>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
|
|
@ -101,6 +104,7 @@ export default function KeypairListComponent({
|
|||
newInputList.splice(index, 1);
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={"minusbtn" + index.toString()}
|
||||
>
|
||||
<IconComponent
|
||||
name="X"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export default function PromptAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: PromptAreaComponentType) {
|
||||
readonly = false,
|
||||
}: PromptAreaComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
onChange("");
|
||||
|
|
@ -22,7 +23,7 @@ export default function PromptAreaComponent({
|
|||
}, [disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== "" && !editNode) {
|
||||
if (value !== "" && !editNode && !readonly) {
|
||||
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
setNodeClass!(apiReturn.data.frontend_node);
|
||||
|
|
@ -35,6 +36,7 @@ export default function PromptAreaComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<GenericModal
|
||||
readonly={readonly}
|
||||
type={TypeModal.PROMPT}
|
||||
value={value}
|
||||
buttonText="Check & Save"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export default function ToggleShadComponent({
|
|||
setEnabled,
|
||||
disabled,
|
||||
size,
|
||||
id = "",
|
||||
}: ToggleComponentType): JSX.Element {
|
||||
let scaleX, scaleY;
|
||||
switch (size) {
|
||||
|
|
@ -29,6 +30,7 @@ export default function ToggleShadComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
||||
<Switch
|
||||
id={id}
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,20 @@ import {
|
|||
} from "../controllers/API";
|
||||
import { APIClassType, APITemplateType } from "../types/api";
|
||||
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 {
|
||||
addVersionToDuplicates,
|
||||
checkOldEdgesHandles,
|
||||
scapeJSONParse,
|
||||
scapedJSONStringfy,
|
||||
updateEdgesHandleIds,
|
||||
updateIds,
|
||||
updateTemplate,
|
||||
} from "../utils/reactflowUtils";
|
||||
|
|
@ -41,7 +51,7 @@ const TabsContextInitialValue: TabsContextType = {
|
|||
isLoading: true,
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: async (flowData?: any) => "",
|
||||
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => uid(),
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
|
|
@ -134,14 +144,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
if (!flow.data) {
|
||||
return;
|
||||
}
|
||||
processFlowEdges(flow);
|
||||
processFlowNodes(flow);
|
||||
processDataFromFlow(flow, false);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
function processFlowEdges(flow: FlowType) {
|
||||
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) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555" };
|
||||
|
|
@ -159,6 +173,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
function processFlowNodes(flow: FlowType) {
|
||||
if (!flow.data || !flow.data.nodes) return;
|
||||
flow.data.nodes.forEach((node: NodeType) => {
|
||||
if (node.data.node?.flow) return;
|
||||
if (skipNodeUpdate.includes(node.data.type)) return;
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
|
|
@ -168,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
if (Object.keys(template["template"]).length > 0) {
|
||||
updateDisplay_name(node, template);
|
||||
updateNodeBaseClasses(node, template);
|
||||
//update baseclasses in edges
|
||||
updateNodeEdges(flow, node, template);
|
||||
updateNodeDescription(node, template);
|
||||
updateNodeTemplate(node, template);
|
||||
|
|
@ -187,11 +203,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
) {
|
||||
flow.data!.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
?.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
sourceHandleObject.baseClasses = template["base_classes"];
|
||||
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.
|
||||
*/
|
||||
async function uploadFlow(
|
||||
newProject?: boolean,
|
||||
newProject: boolean,
|
||||
file?: File
|
||||
): Promise<String | undefined> {
|
||||
let id;
|
||||
|
|
@ -289,7 +306,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
// parse the text into a JSON object
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
|
||||
id = await addFlow(flow, newProject);
|
||||
id = await addFlow(newProject, flow);
|
||||
} else {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
|
|
@ -304,7 +321,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
const currentfile = (e.target as HTMLInputElement).files![0];
|
||||
let text = await currentfile.text();
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
const flowId = await addFlow(flow, newProject);
|
||||
const flowId = await addFlow(newProject, flow);
|
||||
resolve(flowId);
|
||||
}
|
||||
};
|
||||
|
|
@ -355,7 +372,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* Add a new flow to the list of flows.
|
||||
* @param flow Optional flow to add.
|
||||
*/
|
||||
|
||||
function paste(
|
||||
selectionInstance: { nodes: Node[]; edges: Edge[] },
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
|
|
@ -404,19 +420,27 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
reactFlowInstance!.setNodes(nodes);
|
||||
|
||||
selectionInstance.edges.forEach((edge) => {
|
||||
selectionInstance.edges.forEach((edge: Edge) => {
|
||||
let source = idsMap[edge.source];
|
||||
let target = idsMap[edge.target];
|
||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
||||
let sourceHandle =
|
||||
sourceHandleSplitted[0] +
|
||||
"|" +
|
||||
source +
|
||||
"|" +
|
||||
sourceHandleSplitted.slice(2).join("|");
|
||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
||||
let targetHandle =
|
||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
let sourceHandle = scapedJSONStringfy({
|
||||
...sourceHandleObject,
|
||||
id: source,
|
||||
});
|
||||
sourceHandleObject.id = source;
|
||||
edge.data.sourceHandle = sourceHandleObject;
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
let targetHandle = scapedJSONStringfy({
|
||||
...targetHandleObject,
|
||||
id: target,
|
||||
});
|
||||
targetHandleObject.id = target;
|
||||
edge.data.targetHandle = targetHandleObject;
|
||||
let id =
|
||||
"reactflow__edge-" +
|
||||
source +
|
||||
|
|
@ -433,10 +457,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
id,
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
targetHandle.split("|")[0] === "Text"
|
||||
targetHandleObject.type === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ",
|
||||
animated: targetHandle.split("|")[0] === "Text",
|
||||
animated: targetHandleObject.type === "Text",
|
||||
selected: false,
|
||||
},
|
||||
edges.map((edge) => ({ ...edge, selected: false }))
|
||||
|
|
@ -446,19 +470,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
const addFlow = async (
|
||||
flow?: FlowType,
|
||||
newProject?: Boolean
|
||||
newProject: Boolean,
|
||||
flow?: FlowType
|
||||
): Promise<String | undefined> => {
|
||||
if (newProject) {
|
||||
let flowData = extractDataFromFlow(flow!);
|
||||
if (flowData.description == "") {
|
||||
flowData.description = getRandomDescription();
|
||||
}
|
||||
let flowData = flow
|
||||
? processDataFromFlow(flow)
|
||||
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
const newFlow = createNewFlow(flowData, flow!);
|
||||
processFlowEdges(newFlow);
|
||||
processFlowNodes(newFlow);
|
||||
|
||||
const flowName = addVersionToDuplicates(newFlow, flows);
|
||||
|
||||
|
|
@ -486,31 +507,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
};
|
||||
|
||||
const extractDataFromFlow = (flow: FlowType) => {
|
||||
const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
|
||||
let data = flow?.data ? flow.data : null;
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
if (data) {
|
||||
processFlowEdges(flow);
|
||||
processFlowNodes(flow);
|
||||
//add animation to text type edges
|
||||
updateEdges(data.edges);
|
||||
updateNodes(data.nodes, data.edges);
|
||||
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
// updateNodes(data.nodes, data.edges);
|
||||
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
}
|
||||
|
||||
return { data, description };
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateEdges = (edges: Edge[]) => {
|
||||
edges.forEach((edge) => {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
edge.className =
|
||||
(edge.targetHandle!.split("|")[0] === "Text"
|
||||
(targetHandleObject.type === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ") + " stroke-connection";
|
||||
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
|
||||
edge.animated = targetHandleObject.type === "Text";
|
||||
});
|
||||
};
|
||||
|
||||
const updateNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.node?.flow) return;
|
||||
if (skipNodeUpdate.includes(node.data.type)) return;
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
|
|
@ -520,12 +546,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
if (Object.keys(template["template"]).length > 0) {
|
||||
node.data.node.base_classes = template["base_classes"];
|
||||
edges.forEach((edge) => {
|
||||
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge
|
||||
.sourceHandle!.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
let newSourceHandle = sourceHandleObject;
|
||||
newSourceHandle.baseClasses.concat(template["base_classes"]);
|
||||
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||
}
|
||||
});
|
||||
node.data.node.description = template["description"];
|
||||
|
|
@ -538,12 +565,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
};
|
||||
|
||||
const createNewFlow = (
|
||||
flowData: { data: ReactFlowJsonObject | null; description: string },
|
||||
flowData: ReactFlowJsonObject | null,
|
||||
flow: FlowType
|
||||
) => ({
|
||||
description: flowData.description,
|
||||
description: flow?.description ?? getRandomDescription(),
|
||||
name: flow?.name ?? getRandomName(),
|
||||
data: flowData.data,
|
||||
data: flowData,
|
||||
id: "",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import DictComponent from "../../components/dictComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
|
|
@ -166,11 +167,27 @@ const EditNodeModal = forwardRef(
|
|||
.map((templateParam, index) => (
|
||||
<TableRow key={index} className="h-10">
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
{myData.current.node?.template[templateParam].name
|
||||
? myData.current.node.template[templateParam]
|
||||
.name
|
||||
: myData.current.node?.template[templateParam]
|
||||
.display_name}
|
||||
<ShadTooltip
|
||||
content={
|
||||
myData.current.node?.template[templateParam]
|
||||
.proxy
|
||||
? 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 className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||
{myData.current.node?.template[templateParam]
|
||||
|
|
@ -416,6 +433,14 @@ const EditNodeModal = forwardRef(
|
|||
.type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
myData.current.node?.flow &&
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
field_name={templateParam}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
|
|
@ -437,6 +462,14 @@ const EditNodeModal = forwardRef(
|
|||
.type === "code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
myData.current.node?.flow &&
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={
|
||||
data.node!.template[templateParam]
|
||||
.dynamic ?? false
|
||||
|
|
@ -467,6 +500,11 @@ const EditNodeModal = forwardRef(
|
|||
<TableCell className="p-0 text-right">
|
||||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
id={
|
||||
"show" +
|
||||
myData.current.node?.template[templateParam]
|
||||
.name
|
||||
}
|
||||
enabled={
|
||||
!myData.current.node?.template[
|
||||
templateParam
|
||||
|
|
@ -492,6 +530,7 @@ const EditNodeModal = forwardRef(
|
|||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
id={"saveChangesBtn"}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
const newData = cloneDeep(myData.current);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function CodeAreaModal({
|
|||
setNodeClass,
|
||||
children,
|
||||
dynamic,
|
||||
readonly = false,
|
||||
}: codeAreaModalPropsType): JSX.Element {
|
||||
const [code, setCode] = useState(value);
|
||||
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="h-full w-full">
|
||||
<AceEditor
|
||||
readOnly={readonly}
|
||||
value={code}
|
||||
mode="python"
|
||||
height={height ?? "100%"}
|
||||
|
|
@ -180,7 +182,12 @@ export default function CodeAreaModal({
|
|||
</div>
|
||||
</div>
|
||||
<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
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default function GenericModal({
|
|||
nodeClass,
|
||||
setNodeClass,
|
||||
children,
|
||||
readonly = false,
|
||||
}: genericModalPropsType): JSX.Element {
|
||||
const [myButtonText] = useState(buttonText);
|
||||
const [myModalTitle] = useState(modalTitle);
|
||||
|
|
@ -208,7 +209,7 @@ export default function GenericModal({
|
|||
"flex h-full w-full"
|
||||
)}
|
||||
>
|
||||
{type === TypeModal.PROMPT && isEdit ? (
|
||||
{type === TypeModal.PROMPT && isEdit && !readonly ? (
|
||||
<Textarea
|
||||
ref={divRefPrompt}
|
||||
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, "");
|
||||
}}
|
||||
/>
|
||||
) : type === TypeModal.PROMPT && !isEdit ? (
|
||||
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
|
||||
<SanitizedHTMLWrapper
|
||||
className={getClassByNumberLength()}
|
||||
content={coloredContent}
|
||||
|
|
@ -248,6 +249,7 @@ export default function GenericModal({
|
|||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
}}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
|
@ -304,6 +306,7 @@ export default function GenericModal({
|
|||
)}
|
||||
</div>
|
||||
<Button
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
switch (myModalType) {
|
||||
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"
|
||||
className="whitespace-nowrap "
|
||||
onClick={() => {
|
||||
addFlow(flow, true).then((id) => {
|
||||
addFlow(true, flow).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default function DisclosureComponent({
|
|||
<div>
|
||||
<Disclosure.Button className="components-disclosure-arrangement">
|
||||
<div className="flex gap-4">
|
||||
{/* BUG ON THIS ICON */}
|
||||
<Icon strokeWidth={1.5} size={22} className="text-primary" />
|
||||
<span className="components-disclosure-title">{title}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,11 +33,18 @@ import { TabsContext } from "../../../../contexts/tabsContext";
|
|||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { FlowType, NodeType } from "../../../../types/flow";
|
||||
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
|
||||
import { TabsState } from "../../../../types/tabs";
|
||||
import { isValidConnection } from "../../../../utils/reactflowUtils";
|
||||
import { isWrappedWithClass } from "../../../../utils/utils";
|
||||
import {
|
||||
generateFlow,
|
||||
generateNodeFromFlow,
|
||||
isValidConnection,
|
||||
scapeJSONParse,
|
||||
validateSelection,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
import SelectionMenu from "../SelectionMenuComponent";
|
||||
import ExtraSidebar from "../extraSidebarComponent";
|
||||
|
||||
const nodeTypes = {
|
||||
|
|
@ -238,12 +245,19 @@ export default function Page({
|
|||
addEdge(
|
||||
{
|
||||
...params,
|
||||
data: {
|
||||
targetHandle: scapeJSONParse(params.targetHandle!),
|
||||
sourceHandle: scapeJSONParse(params.sourceHandle!),
|
||||
},
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
(params.targetHandle?.split("|")[0] === "Text"
|
||||
((scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||
.type === "Text"
|
||||
? "stroke-foreground "
|
||||
: "stroke-foreground ") + " stroke-connection",
|
||||
animated: params.targetHandle?.split("|")[0] === "Text",
|
||||
animated:
|
||||
(scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||
.type === "Text",
|
||||
},
|
||||
eds
|
||||
)
|
||||
|
|
@ -394,7 +408,7 @@ export default function Page({
|
|||
edgeUpdateSuccessful.current = true;
|
||||
}, []);
|
||||
|
||||
const [selectionEnded, setSelectionEnded] = useState(false);
|
||||
const [selectionEnded, setSelectionEnded] = useState(true);
|
||||
|
||||
const onSelectionEnd = useCallback(() => {
|
||||
setSelectionEnded(true);
|
||||
|
|
@ -434,7 +448,7 @@ export default function Page({
|
|||
<div className="h-full w-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(templates).length > 0 &&
|
||||
Object.keys(types).length > 0 ? (
|
||||
<div className="h-full w-full">
|
||||
<div id="react-flow-id" className="h-full w-full">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onMove={() => {
|
||||
|
|
@ -481,6 +495,50 @@ export default function Page({
|
|||
[&>button]:border-b-border hover:[&>button]:bg-border"
|
||||
></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>
|
||||
{!view && (
|
||||
<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
|
||||
className="extra-side-bar-buttons"
|
||||
onClick={() => {
|
||||
uploadFlow();
|
||||
uploadFlow(false);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileUp" className="side-bar-button-size " />
|
||||
|
|
@ -271,7 +271,13 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
);
|
||||
}}
|
||||
>
|
||||
<div className="side-bar-components-div-form">
|
||||
<div
|
||||
className="side-bar-components-div-form"
|
||||
id={
|
||||
"side" +
|
||||
data[SBSectionName][SBItemName].display_name
|
||||
}
|
||||
>
|
||||
<span className="side-bar-components-text">
|
||||
{data[SBSectionName][SBItemName].display_name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,17 @@ import {
|
|||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import EditNodeModal from "../../../../modals/EditNodeModal";
|
||||
import { nodeToolbarPropsType } from "../../../../types/components";
|
||||
import {
|
||||
expandGroupNode,
|
||||
updateFlowPosition,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
|
||||
|
||||
export default function NodeToolbarComponent({
|
||||
data,
|
||||
setData,
|
||||
deleteNode,
|
||||
position,
|
||||
setShowNode,
|
||||
numberOfHandles,
|
||||
showNode,
|
||||
|
|
@ -47,6 +52,8 @@ export default function NodeToolbarComponent({
|
|||
return true;
|
||||
}
|
||||
const isMinimal = canMinimize();
|
||||
const isGroup = data.node?.flow ? true : false;
|
||||
|
||||
const { paste } = useContext(TabsContext);
|
||||
const reactFlowInstance = useReactFlow();
|
||||
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
||||
|
|
@ -65,6 +72,10 @@ export default function NodeToolbarComponent({
|
|||
if (event.includes("disabled")) {
|
||||
return;
|
||||
}
|
||||
if (event.includes("ungroup")) {
|
||||
updateFlowPosition(position, data.node?.flow!);
|
||||
expandGroupNode(data, reactFlowInstance);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -134,11 +145,11 @@ export default function NodeToolbarComponent({
|
|||
</a>
|
||||
</ShadTooltip>
|
||||
|
||||
{isMinimal ? (
|
||||
{isMinimal || isGroup ? (
|
||||
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
||||
<ShadTooltip content="More" side="top">
|
||||
<SelectTrigger>
|
||||
<div>
|
||||
<div id="advancedIcon">
|
||||
<div
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
|
||||
|
|
@ -163,6 +174,7 @@ export default function NodeToolbarComponent({
|
|||
}
|
||||
>
|
||||
<div
|
||||
id="editAdvancedBtn"
|
||||
className={
|
||||
"flex " +
|
||||
(nodeLength == 0
|
||||
|
|
@ -179,7 +191,7 @@ export default function NodeToolbarComponent({
|
|||
</SelectItem>
|
||||
{isMinimal && (
|
||||
<SelectItem value={getRandomKeyByssmm() + "show"}>
|
||||
<div className="flex">
|
||||
<div className="flex" id="editAdvanced">
|
||||
<IconComponent
|
||||
name={showNode ? "Minimize2" : "Maximize2"}
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
|
|
@ -188,11 +200,22 @@ export default function NodeToolbarComponent({
|
|||
</div>
|
||||
</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>
|
||||
</Select>
|
||||
) : (
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<div>
|
||||
<div id="editAdvancedIcon">
|
||||
<button
|
||||
disabled={nodeLength === 0}
|
||||
onClick={() => setShowModalAdvanced(true)}
|
||||
|
|
@ -222,29 +245,6 @@ export default function NodeToolbarComponent({
|
|||
<></>
|
||||
</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>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export default function HomePage(): JSX.Element {
|
|||
<DropdownButton
|
||||
firstButtonName="New Project"
|
||||
onFirstBtnClick={() => {
|
||||
addFlow(null!, true).then((id) => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
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
|
||||
export type APIDataType = { [key: string]: APIKindType };
|
||||
export type APIObjectType = { kind: APIKindType; [key: string]: APIKindType };
|
||||
export type APIKindType = { class: APIClassType; [key: string]: APIClassType };
|
||||
export type APITemplateType = {
|
||||
variable: TemplateVariableType;
|
||||
[key: string]: TemplateVariableType;
|
||||
};
|
||||
|
||||
|
|
@ -23,10 +23,13 @@ export type APIClassType = {
|
|||
beta?: boolean;
|
||||
documentation: string;
|
||||
error?: string;
|
||||
flow?: FlowType;
|
||||
[key: string]:
|
||||
| Array<string>
|
||||
| string
|
||||
| APITemplateType
|
||||
| boolean
|
||||
| FlowType
|
||||
| CustomFieldsType
|
||||
| boolean
|
||||
| undefined;
|
||||
|
|
@ -38,8 +41,11 @@ export type TemplateVariableType = {
|
|||
placeholder?: string;
|
||||
list: boolean;
|
||||
show: boolean;
|
||||
readonly: boolean;
|
||||
multiline?: boolean;
|
||||
value?: any;
|
||||
dynamic?: boolean;
|
||||
proxy?: { id: string; field: string };
|
||||
input_types?: Array<string>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { ReactElement, ReactNode } from "react";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
import { ReactFlowJsonObject, XYPosition } from "reactflow";
|
||||
import { APIClassType, APITemplateType, TemplateVariableType } from "../api";
|
||||
import { ChatMessageType } from "../chat";
|
||||
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
|
||||
import { typesContextType } from "../typesContext";
|
||||
|
||||
import { sourceHandleType, targetHandleType } from "./../flow/index";
|
||||
export type InputComponentType = {
|
||||
autoFocus?: boolean;
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
|
|
@ -17,12 +19,14 @@ export type InputComponentType = {
|
|||
showPass?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
blurOnEnter?: boolean;
|
||||
};
|
||||
export type ToggleComponentType = {
|
||||
enabled: boolean;
|
||||
setEnabled: (state: boolean) => void;
|
||||
disabled: boolean | undefined;
|
||||
size: "small" | "medium" | "large";
|
||||
id?: string;
|
||||
};
|
||||
export type DropDownComponentType = {
|
||||
value: string;
|
||||
|
|
@ -36,7 +40,7 @@ export type ParameterComponentType = {
|
|||
data: NodeDataType;
|
||||
setData: (value: NodeDataType) => void;
|
||||
title: string;
|
||||
id: string;
|
||||
id: sourceHandleType | targetHandleType;
|
||||
color: string;
|
||||
left: boolean;
|
||||
type: string | undefined;
|
||||
|
|
@ -46,6 +50,7 @@ export type ParameterComponentType = {
|
|||
dataContext?: typesContextType;
|
||||
optionalHandle?: Array<String> | null;
|
||||
info?: string;
|
||||
proxy?: { field: string; id: string };
|
||||
showNode?: boolean;
|
||||
};
|
||||
export type InputListComponentType = {
|
||||
|
|
@ -80,6 +85,7 @@ export type TextAreaComponentType = {
|
|||
onChange: (value: string[] | string) => void;
|
||||
value: string;
|
||||
editNode?: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export type PromptAreaComponentType = {
|
||||
|
|
@ -89,6 +95,7 @@ export type PromptAreaComponentType = {
|
|||
disabled: boolean;
|
||||
onChange: (value: string[] | string) => void;
|
||||
value: string;
|
||||
readonly?: boolean;
|
||||
editNode?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -100,6 +107,7 @@ export type CodeAreaComponentType = {
|
|||
nodeClass?: APIClassType;
|
||||
setNodeClass?: (value: APIClassType) => void;
|
||||
dynamic?: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export type FileComponentType = {
|
||||
|
|
@ -426,6 +434,7 @@ export type nodeToolbarPropsType = {
|
|||
data: NodeDataType;
|
||||
deleteNode: (idx: string) => void;
|
||||
setData: (newState: NodeDataType) => void;
|
||||
position: XYPosition;
|
||||
setShowNode: (boolean: any) => void;
|
||||
numberOfHandles: boolean[] | [];
|
||||
showNode: boolean;
|
||||
|
|
@ -461,6 +470,7 @@ export type codeAreaModalPropsType = {
|
|||
setNodeClass: (Class: APIClassType) => void | undefined;
|
||||
children: ReactNode;
|
||||
dynamic?: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export type chatMessagePropsType = {
|
||||
|
|
@ -485,6 +495,7 @@ export type genericModalPropsType = {
|
|||
nodeClass?: APIClassType;
|
||||
setNodeClass?: (Class: APIClassType) => void;
|
||||
children: ReactNode;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export type buttonBoxPropsType = {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ export type NodeType = {
|
|||
type?: string;
|
||||
position: XYPosition;
|
||||
data: NodeDataType;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
export type NodeDataType = {
|
||||
type: string;
|
||||
node?: APIClassType;
|
||||
id: string;
|
||||
output_types?: string[];
|
||||
};
|
||||
// FlowStyleType is the type of the style object that is used to style the
|
||||
// Flow card with an emoji and a color.
|
||||
|
|
@ -35,3 +37,18 @@ export type TweaksType = Array<
|
|||
};
|
||||
} & 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>;
|
||||
removeFlow: (id: string) => void;
|
||||
addFlow: (
|
||||
flow?: FlowType,
|
||||
newProject?: Boolean
|
||||
newProject: boolean,
|
||||
flow?: FlowType
|
||||
) => Promise<String | undefined>;
|
||||
updateFlow: (newFlow: FlowType) => void;
|
||||
incrementNodeId: () => string;
|
||||
|
|
@ -23,7 +23,7 @@ export type TabsContextType = {
|
|||
uploadFlows: () => void;
|
||||
isBuilt: boolean;
|
||||
setIsBuilt: (state: boolean) => void;
|
||||
uploadFlow: (newFlow?: boolean, file?: File) => Promise<String | undefined>;
|
||||
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
|
||||
hardReset: () => void;
|
||||
getNodeId: (nodeType: string) => string;
|
||||
tabsState: TabsState;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Edge, Node } from "reactflow";
|
||||
import { NodeType } from "../flow";
|
||||
import { FlowType, NodeType } from "../flow";
|
||||
|
||||
export type cleanEdgesType = {
|
||||
flow: {
|
||||
|
|
@ -11,5 +11,17 @@ export type cleanEdgesType = {
|
|||
|
||||
export type unselectAllNodesType = {
|
||||
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,
|
||||
Edge,
|
||||
Node,
|
||||
OnSelectionChangeParams,
|
||||
ReactFlowInstance,
|
||||
ReactFlowJsonObject,
|
||||
XYPosition,
|
||||
} from "reactflow";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { specialCharsRegex } from "../constants/constants";
|
||||
import { APITemplateType } from "../types/api";
|
||||
import { FlowType, NodeType } from "../types/flow";
|
||||
import { APITemplateType, TemplateVariableType } from "../types/api";
|
||||
import {
|
||||
FlowType,
|
||||
NodeDataType,
|
||||
NodeType,
|
||||
sourceHandleType,
|
||||
targetHandleType,
|
||||
} from "../types/flow";
|
||||
import {
|
||||
cleanEdgesType,
|
||||
findLastNodeType,
|
||||
generateFlowType,
|
||||
unselectAllNodesType,
|
||||
updateEdgesHandleIdsType,
|
||||
} from "../types/utils/reactflowUtils";
|
||||
import { toNormalCase } from "./utils";
|
||||
import { toNormalCase, toTitleCase } from "./utils";
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
export function cleanEdges({
|
||||
flow: { edges, nodes },
|
||||
|
|
@ -32,26 +45,30 @@ export function cleanEdges({
|
|||
const sourceHandle = edge.sourceHandle; //right
|
||||
const targetHandle = edge.targetHandle; //left
|
||||
if (targetHandle) {
|
||||
const field = targetHandle.split("|")[1];
|
||||
const id =
|
||||
(targetNode.data.node?.template[field]?.input_types?.join(";") ??
|
||||
targetNode.data.node?.template[field]?.type) +
|
||||
"|" +
|
||||
field +
|
||||
"|" +
|
||||
targetNode.data.id;
|
||||
if (id !== targetHandle) {
|
||||
const targetHandleObject: targetHandleType =
|
||||
scapeJSONParse(targetHandle);
|
||||
const field = targetHandleObject.fieldName;
|
||||
const id: targetHandleType = {
|
||||
type: targetNode.data.node!.template[field]?.type,
|
||||
fieldName: field,
|
||||
id: targetNode.data.id,
|
||||
inputTypes: targetNode.data.node!.template[field]?.input_types,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (sourceHandle) {
|
||||
const id = [
|
||||
sourceNode.data.type,
|
||||
sourceNode.data.id,
|
||||
...sourceNode.data.node?.base_classes!,
|
||||
].join("|");
|
||||
if (id !== sourceHandle) {
|
||||
newEdges = newEdges.filter((edg) => edg.id !== edge.id);
|
||||
const id: sourceHandleType = {
|
||||
id: sourceNode.data.id,
|
||||
baseClasses: sourceNode.data.node!.base_classes,
|
||||
dataType: sourceNode.data.type,
|
||||
};
|
||||
if (scapedJSONStringfy(id) !== sourceHandle) {
|
||||
newEdges = newEdges.filter((e) => e.id !== edge.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +78,7 @@ export function cleanEdges({
|
|||
|
||||
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
|
||||
let newNodes = _.cloneDeep(data);
|
||||
newNodes!.forEach((node: Node) => {
|
||||
newNodes.forEach((node: Node) => {
|
||||
node.selected = false;
|
||||
});
|
||||
updateNodes(newNodes!);
|
||||
|
|
@ -71,21 +88,18 @@ export function isValidConnection(
|
|||
{ source, target, sourceHandle, targetHandle }: Connection,
|
||||
reactFlowInstance: ReactFlowInstance
|
||||
) {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);
|
||||
if (
|
||||
targetHandle
|
||||
?.split("|")[0]
|
||||
.split(";")
|
||||
.some((target) => target === sourceHandle?.split("|")[0]) ||
|
||||
sourceHandle
|
||||
?.split("|")
|
||||
.slice(2)
|
||||
.some((target) =>
|
||||
targetHandle
|
||||
?.split("|")[0]
|
||||
.split(";")
|
||||
.some((n) => n === target)
|
||||
) ||
|
||||
targetHandle?.split("|")[0] === "str"
|
||||
targetHandleObject.inputTypes?.some(
|
||||
(n) => n === sourceHandleObject.dataType
|
||||
) ||
|
||||
sourceHandleObject.baseClasses.some(
|
||||
(t) =>
|
||||
targetHandleObject.inputTypes?.some((n) => n === t) ||
|
||||
t === targetHandleObject.type
|
||||
) ||
|
||||
targetHandleObject.type === "str"
|
||||
) {
|
||||
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
|
||||
if (!targetNode) {
|
||||
|
|
@ -97,11 +111,11 @@ export function isValidConnection(
|
|||
return true;
|
||||
}
|
||||
} else if (
|
||||
(!targetNode.template[targetHandle?.split("|")[1]!].list &&
|
||||
(!targetNode.template[targetHandleObject.fieldName].list &&
|
||||
!reactFlowInstance
|
||||
.getEdges()
|
||||
.find((e) => e.targetHandle === targetHandle)) ||
|
||||
targetNode.template[targetHandle?.split("|")[1]!].list
|
||||
targetNode.template[targetHandleObject.fieldName].list
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -152,26 +166,36 @@ export function updateIds(
|
|||
|
||||
newFlow.nodes.forEach((node: NodeType) => {
|
||||
// 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;
|
||||
node.id = newId;
|
||||
node.data.id = newId;
|
||||
// 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.target = idsMap[edge.target];
|
||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
||||
edge.sourceHandle =
|
||||
sourceHandleSplitted[0] +
|
||||
"|" +
|
||||
edge.source +
|
||||
"|" +
|
||||
sourceHandleSplitted.slice(2).join("|");
|
||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
||||
edge.targetHandle =
|
||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + edge.target;
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
edge.sourceHandle = scapedJSONStringfy({
|
||||
...sourceHandleObject,
|
||||
id: edge.source,
|
||||
});
|
||||
if (edge.data?.sourceHandle?.id) {
|
||||
edge.data.sourceHandle.id = edge.source;
|
||||
}
|
||||
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 =
|
||||
"reactflow__edge-" +
|
||||
edge.source +
|
||||
|
|
@ -210,8 +234,10 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
|||
template[t].value === "") &&
|
||||
!edges.some(
|
||||
(edge) =>
|
||||
edge.targetHandle?.split("|")[1] === t &&
|
||||
edge.targetHandle.split("|")[2] === node.id
|
||||
(scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
|
||||
t &&
|
||||
(scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
|
||||
node.id
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
|
|
@ -266,6 +292,49 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
|
|||
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(
|
||||
e:
|
||||
| React.KeyboardEvent<HTMLInputElement>
|
||||
|
|
@ -377,3 +446,592 @@ export function convertValuesToNumbers(arr) {
|
|||
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,
|
||||
GitFork,
|
||||
GithubIcon,
|
||||
Group,
|
||||
Hammer,
|
||||
HelpCircle,
|
||||
Home,
|
||||
|
|
@ -70,6 +71,7 @@ import {
|
|||
TerminalSquare,
|
||||
Trash2,
|
||||
Undo,
|
||||
Ungroup,
|
||||
Unplug,
|
||||
Upload,
|
||||
User,
|
||||
|
|
@ -312,8 +314,10 @@ export const nodeIconsLucide: iconsType = {
|
|||
UserCog2,
|
||||
Key,
|
||||
Unplug,
|
||||
BookMarked,
|
||||
Group,
|
||||
ChevronUp,
|
||||
Ungroup,
|
||||
BookMarked,
|
||||
Minus,
|
||||
Square,
|
||||
Minimize2,
|
||||
|
|
|
|||
|
|
@ -449,6 +449,29 @@ chat_input_field: Input key that you want the chat to send the user message with
|
|||
></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) {
|
||||
if (!method) return;
|
||||
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");
|
||||
});
|
||||
});
|
||||
563
src/frontend/tests/keypair.spec.ts
Normal file
563
src/frontend/tests/keypair.spec.ts
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("KeypairListComponent", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.getByPlaceholder("Search").click();
|
||||
await page.getByPlaceholder("Search").fill("csv");
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="sideCSVLoader"]')
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
|
||||
await page.locator('//*[@id="keypair0"]').click();
|
||||
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
|
||||
await page.locator('//*[@id="keypair100"]').click();
|
||||
await page.locator('//*[@id="keypair100"]').fill("testtesttesttesttesttest");
|
||||
|
||||
const plusButtonLocatorNode = page.locator('//*[@id="plusbtn0"]');
|
||||
const elementCountNode = await plusButtonLocatorNode.count();
|
||||
if (elementCountNode > 0) {
|
||||
await plusButtonLocatorNode.click();
|
||||
}
|
||||
|
||||
await page.locator('//*[@id="keypair1"]').click();
|
||||
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
|
||||
await page.locator('//*[@id="keypair101"]').click();
|
||||
await page.locator('//*[@id="keypair101"]').fill("testtesttesttesttesttest1");
|
||||
await page.locator('//*[@id="plusbtn1"]').click();
|
||||
|
||||
await page.locator('//*[@id="keypair2"]').click();
|
||||
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
|
||||
await page.locator('//*[@id="keypair102"]').click();
|
||||
await page.locator('//*[@id="keypair102"]').fill("testtesttesttesttesttest2");
|
||||
|
||||
await page.locator('//*[@id="minusbtn1"]').click();
|
||||
|
||||
const keyPairVerification = page.locator('//*[@id="keypair102"]');
|
||||
const elementKeyCount = await keyPairVerification.count();
|
||||
|
||||
if (elementKeyCount === 0) {
|
||||
expect(true).toBeTruthy();
|
||||
} else {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
|
||||
)
|
||||
.click();
|
||||
await page.locator('//*[@id="advancedIcon"]').click();
|
||||
await page.locator('//*[@id="editAdvancedBtn"]').click();
|
||||
|
||||
await page.locator('//*[@id="showfile_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showfile_path"]').isChecked()
|
||||
).toBeFalsy();
|
||||
await page.locator('//*[@id="showmetadata"]').click();
|
||||
expect(await page.locator('//*[@id="showmetadata"]').isChecked()).toBeFalsy();
|
||||
await page.locator('//*[@id="saveChangesBtn"]').click();
|
||||
|
||||
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
|
||||
const elementCount = await plusButtonLocator.count();
|
||||
if (elementCount === 0) {
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
|
||||
)
|
||||
.click();
|
||||
await page.locator('//*[@id="advancedIcon"]').click();
|
||||
await page.locator('//*[@id="editAdvancedBtn"]').click();
|
||||
|
||||
await page.locator('//*[@id="showfile_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showfile_path"]').isChecked()
|
||||
).toBeTruthy();
|
||||
await page.locator('//*[@id="showmetadata"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmetadata"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
await page.locator('//*[@id="keypair0"]').click();
|
||||
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
|
||||
await page.locator('//*[@id="keypair100"]').click();
|
||||
await page
|
||||
.locator('//*[@id="keypair100"]')
|
||||
.fill("testtesttesttesttesttest");
|
||||
|
||||
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
|
||||
const elementCount = await plusButtonLocator.count();
|
||||
if (elementCount > 0) {
|
||||
await plusButtonLocator.click();
|
||||
}
|
||||
|
||||
await page.locator('//*[@id="keypair1"]').click();
|
||||
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
|
||||
await page.locator('//*[@id="keypair101"]').click();
|
||||
await page
|
||||
.locator('//*[@id="keypair101"]')
|
||||
.fill("testtesttesttesttesttest1");
|
||||
await page.locator('//*[@id="plusbtn1"]').click();
|
||||
|
||||
await page.locator('//*[@id="keypair2"]').click();
|
||||
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
|
||||
await page.locator('//*[@id="keypair102"]').click();
|
||||
await page
|
||||
.locator('//*[@id="keypair102"]')
|
||||
.fill("testtesttesttesttesttest2");
|
||||
|
||||
await page.locator('//*[@id="minusbtn1"]').click();
|
||||
|
||||
const keyPairVerification = page.locator('//*[@id="keypair102"]');
|
||||
const elementKeyCount = await keyPairVerification.count();
|
||||
|
||||
if (elementKeyCount === 0) {
|
||||
await page.locator('//*[@id="saveChangesBtn"]').click();
|
||||
|
||||
const key1 = await page.locator('//*[@id="keypair0"]').inputValue();
|
||||
const value1 = await page.locator('//*[@id="keypair100"]').inputValue();
|
||||
const key2 = await page.locator('//*[@id="keypair1"]').inputValue();
|
||||
const value2 = await page.locator('//*[@id="keypair101"]').inputValue();
|
||||
|
||||
if (
|
||||
key1 === "testtesttesttest" &&
|
||||
value1 === "testtesttesttesttesttest" &&
|
||||
key2 === "testtesttesttest2" &&
|
||||
value2 === "testtesttesttesttesttest2"
|
||||
) {
|
||||
expect(true).toBeTruthy();
|
||||
} else {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
} else {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
} else {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test("FloatComponent", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.getByPlaceholder("Search").click();
|
||||
await page.getByPlaceholder("Search").fill("llamacpp");
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="sideLlamaCpp"]')
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
|
||||
await page.locator('//*[@id="float-input"]').click();
|
||||
await page.locator('//*[@id="float-input"]').fill("3");
|
||||
|
||||
let value = await page.locator('//*[@id="float-input"]').inputValue();
|
||||
|
||||
if (value != "2") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page.locator('//*[@id="float-input"]').click();
|
||||
await page.locator('//*[@id="float-input"]').fill("-3");
|
||||
|
||||
value = await page.locator('//*[@id="float-input"]').inputValue();
|
||||
|
||||
if (value != "-2") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
|
||||
)
|
||||
.click();
|
||||
await page.locator('//*[@id="editAdvancedIcon"]').click();
|
||||
|
||||
await page.locator('//*[@id="showcache"]').click();
|
||||
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showecho
|
||||
await page.locator('//*[@id="showecho"]').click();
|
||||
expect(await page.locator('//*[@id="showecho"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showf16_kv
|
||||
await page.locator('//*[@id="showf16_kv"]').click();
|
||||
expect(await page.locator('//*[@id="showf16_kv"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showgrammar_path
|
||||
await page.locator('//*[@id="showgrammar_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showgrammar_path"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showlast_n_tokens_size
|
||||
await page.locator('//*[@id="showlast_n_tokens_size"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlast_n_tokens_size"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showlogits_all
|
||||
await page.locator('//*[@id="showlogits_all"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlogits_all"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showlogprobs
|
||||
await page.locator('//*[@id="showlogprobs"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlogprobs"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showlora_base
|
||||
await page.locator('//*[@id="showlora_base"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlora_base"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showlora_path
|
||||
await page.locator('//*[@id="showlora_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlora_path"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showmax_tokens
|
||||
await page.locator('//*[@id="showmax_tokens"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmax_tokens"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showmetadata
|
||||
await page.locator('//*[@id="showmetadata"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmetadata"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showmodel_kwargs
|
||||
await page.locator('//*[@id="showmodel_kwargs"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmodel_kwargs"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showmodel_path
|
||||
await page.locator('//*[@id="showmodel_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmodel_path"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// shown_batch
|
||||
await page.locator('//*[@id="shown_batch"]').click();
|
||||
expect(await page.locator('//*[@id="shown_batch"]').isChecked()).toBeTruthy();
|
||||
|
||||
// shown_ctx
|
||||
await page.locator('//*[@id="shown_ctx"]').click();
|
||||
expect(await page.locator('//*[@id="shown_ctx"]').isChecked()).toBeTruthy();
|
||||
|
||||
// shown_gpu_layers
|
||||
await page.locator('//*[@id="shown_gpu_layers"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="shown_gpu_layers"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// shown_parts
|
||||
await page.locator('//*[@id="shown_parts"]').click();
|
||||
expect(await page.locator('//*[@id="shown_parts"]').isChecked()).toBeTruthy();
|
||||
|
||||
// shown_threads
|
||||
await page.locator('//*[@id="shown_threads"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="shown_threads"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showrepeat_penalty
|
||||
await page.locator('//*[@id="showrepeat_penalty"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showrepeat_penalty"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showrope_freq_base
|
||||
await page.locator('//*[@id="showrope_freq_base"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showrope_freq_base"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showrope_freq_scale
|
||||
await page.locator('//*[@id="showrope_freq_scale"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showrope_freq_scale"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showseed
|
||||
await page.locator('//*[@id="showseed"]').click();
|
||||
expect(await page.locator('//*[@id="showseed"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showstop
|
||||
await page.locator('//*[@id="showstop"]').click();
|
||||
expect(await page.locator('//*[@id="showstop"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showstreaming
|
||||
await page.locator('//*[@id="showstreaming"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showstreaming"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showsuffix
|
||||
await page.locator('//*[@id="showsuffix"]').click();
|
||||
expect(await page.locator('//*[@id="showsuffix"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showtags
|
||||
await page.locator('//*[@id="showtags"]').click();
|
||||
expect(await page.locator('//*[@id="showtags"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showtemperature
|
||||
await page.locator('//*[@id="showtemperature"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showtemperature"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showtop_k
|
||||
await page.locator('//*[@id="showtop_k"]').click();
|
||||
expect(await page.locator('//*[@id="showtop_k"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showtop_p
|
||||
await page.locator('//*[@id="showtop_p"]').click();
|
||||
expect(await page.locator('//*[@id="showtop_p"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showuse_mlock
|
||||
await page.locator('//*[@id="showuse_mlock"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showuse_mlock"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showuse_mmap
|
||||
await page.locator('//*[@id="showuse_mmap"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showuse_mmap"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showverbose
|
||||
await page.locator('//*[@id="showverbose"]').click();
|
||||
expect(await page.locator('//*[@id="showverbose"]').isChecked()).toBeTruthy();
|
||||
|
||||
// showvocab_only
|
||||
await page.locator('//*[@id="showvocab_only"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showvocab_only"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
await page.locator('//*[@id="showcache"]').click();
|
||||
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showecho
|
||||
await page.locator('//*[@id="showecho"]').click();
|
||||
expect(await page.locator('//*[@id="showecho"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showf16_kv
|
||||
await page.locator('//*[@id="showf16_kv"]').click();
|
||||
expect(await page.locator('//*[@id="showf16_kv"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showgrammar_path
|
||||
await page.locator('//*[@id="showgrammar_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showgrammar_path"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showlast_n_tokens_size
|
||||
await page.locator('//*[@id="showlast_n_tokens_size"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlast_n_tokens_size"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showlogits_all
|
||||
await page.locator('//*[@id="showlogits_all"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlogits_all"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showlogprobs
|
||||
await page.locator('//*[@id="showlogprobs"]').click();
|
||||
expect(await page.locator('//*[@id="showlogprobs"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showlora_base
|
||||
await page.locator('//*[@id="showlora_base"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlora_base"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showlora_path
|
||||
await page.locator('//*[@id="showlora_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showlora_path"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showmax_tokens
|
||||
await page.locator('//*[@id="showmax_tokens"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmax_tokens"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showmetadata
|
||||
await page.locator('//*[@id="showmetadata"]').click();
|
||||
expect(await page.locator('//*[@id="showmetadata"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showmodel_kwargs
|
||||
await page.locator('//*[@id="showmodel_kwargs"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmodel_kwargs"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showmodel_path
|
||||
await page.locator('//*[@id="showmodel_path"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmodel_path"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// shown_batch
|
||||
await page.locator('//*[@id="shown_batch"]').click();
|
||||
expect(await page.locator('//*[@id="shown_batch"]').isChecked()).toBeFalsy();
|
||||
|
||||
// shown_ctx
|
||||
await page.locator('//*[@id="shown_ctx"]').click();
|
||||
expect(await page.locator('//*[@id="shown_ctx"]').isChecked()).toBeFalsy();
|
||||
|
||||
// shown_gpu_layers
|
||||
await page.locator('//*[@id="shown_gpu_layers"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="shown_gpu_layers"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// shown_parts
|
||||
await page.locator('//*[@id="shown_parts"]').click();
|
||||
expect(await page.locator('//*[@id="shown_parts"]').isChecked()).toBeFalsy();
|
||||
|
||||
// shown_threads
|
||||
await page.locator('//*[@id="shown_threads"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="shown_threads"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showrepeat_penalty
|
||||
await page.locator('//*[@id="showrepeat_penalty"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showrepeat_penalty"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showrope_freq_base
|
||||
await page.locator('//*[@id="showrope_freq_base"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showrope_freq_base"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showrope_freq_scale
|
||||
await page.locator('//*[@id="showrope_freq_scale"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showrope_freq_scale"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showseed
|
||||
await page.locator('//*[@id="showseed"]').click();
|
||||
expect(await page.locator('//*[@id="showseed"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showstop
|
||||
await page.locator('//*[@id="showstop"]').click();
|
||||
expect(await page.locator('//*[@id="showstop"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showstreaming
|
||||
await page.locator('//*[@id="showstreaming"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showstreaming"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showsuffix
|
||||
await page.locator('//*[@id="showsuffix"]').click();
|
||||
expect(await page.locator('//*[@id="showsuffix"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showtags
|
||||
await page.locator('//*[@id="showtags"]').click();
|
||||
expect(await page.locator('//*[@id="showtags"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showtop_k
|
||||
await page.locator('//*[@id="showtop_k"]').click();
|
||||
expect(await page.locator('//*[@id="showtop_k"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showtop_p
|
||||
await page.locator('//*[@id="showtop_p"]').click();
|
||||
expect(await page.locator('//*[@id="showtop_p"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showuse_mlock
|
||||
await page.locator('//*[@id="showuse_mlock"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showuse_mlock"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showuse_mmap
|
||||
await page.locator('//*[@id="showuse_mmap"]').click();
|
||||
expect(await page.locator('//*[@id="showuse_mmap"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showverbose
|
||||
await page.locator('//*[@id="showverbose"]').click();
|
||||
expect(await page.locator('//*[@id="showverbose"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showvocab_only
|
||||
await page.locator('//*[@id="showvocab_only"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showvocab_only"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
await page.locator('//*[@id="saveChangesBtn"]').click();
|
||||
|
||||
const plusButtonLocator = page.locator('//*[@id="float-input"]');
|
||||
const elementCount = await plusButtonLocator.count();
|
||||
if (elementCount === 0) {
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
|
||||
)
|
||||
.click();
|
||||
await page.locator('//*[@id="editAdvancedIcon"]').click();
|
||||
|
||||
// showtemperature
|
||||
await page.locator('//*[@id="showtemperature"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showtemperature"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
await page.locator('//*[@id="saveChangesBtn"]').click();
|
||||
await page.locator('//*[@id="float-input"]').click();
|
||||
await page.locator('//*[@id="float-input"]').fill("3");
|
||||
|
||||
let value = await page.locator('//*[@id="float-input"]').inputValue();
|
||||
|
||||
if (value != "2") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page.locator('//*[@id="float-input"]').click();
|
||||
await page.locator('//*[@id="float-input"]').fill("-3");
|
||||
|
||||
value = await page.locator('//*[@id="float-input"]').inputValue();
|
||||
|
||||
if (value != "-2") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -35,6 +35,16 @@ def pytest_configure():
|
|||
pytest.OPENAPI_EXAMPLE_PATH = (
|
||||
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 = (
|
||||
Path(__file__).parent.absolute() / "data" / "BasicChatwithPromptandHistory.json"
|
||||
)
|
||||
|
|
@ -159,6 +169,24 @@ def json_flow():
|
|||
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
|
||||
def json_flow_with_prompt_and_history():
|
||||
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 os
|
||||
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.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
|
||||
|
||||
|
|
@ -24,6 +34,52 @@ from langflow.utils.payload import get_root_node
|
|||
# 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]:
|
||||
"""Get a node by type"""
|
||||
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):
|
||||
"""Test getting a single node"""
|
||||
node_id = basic_graph.nodes[0].id
|
||||
|
|
@ -322,6 +329,196 @@ def test_get_result_and_thought(basic_graph):
|
|||
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):
|
||||
loaded_json = json.loads(json_vector_store)
|
||||
graph = Graph.from_payload(loaded_json)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
|||
template = prompt["template"]
|
||||
assert template["input_variables"] == {
|
||||
"required": True,
|
||||
"dynamic": False,
|
||||
"dynamic": True,
|
||||
"placeholder": "",
|
||||
"show": False,
|
||||
"multiline": False,
|
||||
|
|
@ -35,7 +35,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
|||
|
||||
assert template["output_parser"] == {
|
||||
"required": False,
|
||||
"dynamic": False,
|
||||
"dynamic": True,
|
||||
"placeholder": "",
|
||||
"show": False,
|
||||
"multiline": False,
|
||||
|
|
@ -49,7 +49,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
|||
|
||||
assert template["partial_variables"] == {
|
||||
"required": False,
|
||||
"dynamic": False,
|
||||
"dynamic": True,
|
||||
"placeholder": "",
|
||||
"show": False,
|
||||
"multiline": False,
|
||||
|
|
@ -63,7 +63,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
|||
|
||||
assert template["template"] == {
|
||||
"required": True,
|
||||
"dynamic": False,
|
||||
"dynamic": True,
|
||||
"placeholder": "",
|
||||
"show": True,
|
||||
"multiline": True,
|
||||
|
|
@ -77,7 +77,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
|||
|
||||
assert template["template_format"] == {
|
||||
"required": False,
|
||||
"dynamic": False,
|
||||
"dynamic": True,
|
||||
"placeholder": "",
|
||||
"show": False,
|
||||
"multiline": False,
|
||||
|
|
@ -92,7 +92,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
|||
|
||||
assert template["validate_template"] == {
|
||||
"required": False,
|
||||
"dynamic": False,
|
||||
"dynamic": True,
|
||||
"placeholder": "",
|
||||
"show": False,
|
||||
"multiline": False,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue