Generic Loaders (#964)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-11-01 11:15:22 -03:00 • committed by GitHub
commit 07cfca4949
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
128 changed files with 10995 additions and 2067 deletions

View file

@ -15,7 +15,7 @@
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "make install_frontend && make install_backend", "postCreateCommand": "make setup_devcontainer",
"containerEnv": { "containerEnv": {
"POETRY_VIRTUALENVS_IN_PROJECT": "true" "POETRY_VIRTUALENVS_IN_PROJECT": "true"
@ -31,7 +31,9 @@
"sourcery.sourcery", "sourcery.sourcery",
"eamodio.gitlens", "eamodio.gitlens",
"ms-vscode.makefile-tools", "ms-vscode.makefile-tools",
"GitHub.vscode-pull-request-github" "GitHub.vscode-pull-request-github",
"Codium.codium",
"ms-azuretools.vscode-docker"
] ]
} }
} }

View file

@ -56,6 +56,14 @@ LANGFLOW_REMOVE_API_KEYS=
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600) # LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
LANGFLOW_CACHE_TYPE= LANGFLOW_CACHE_TYPE=
# Auto login
# If set to true then a superuser will be logged in automatically
# and the login page will be skipped, keeping the
# default experience of Langflow
# Values: true, false
# Example: LANGFLOW_AUTO_LOGIN=true
LANGFLOW_AUTO_LOGIN=
# Superuser username # Superuser username
# Example: LANGFLOW_SUPERUSER=admin # Example: LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER= LANGFLOW_SUPERUSER=

View file

@ -4,10 +4,18 @@ on:
push: push:
branches: branches:
- dev - dev
paths:
- "tests/**"
- "src/backend/**"
- "pyproject.toml"
pull_request: pull_request:
branches: branches:
- dev - dev
- main - main
paths:
- "tests/**"
- "src/backend/**"
- "pyproject.toml"
jobs: jobs:
build-and-test: build-and-test:

View file

@ -3,7 +3,16 @@ name: lint
on: on:
push: push:
branches: [main] branches: [main]
paths:
- "tests/**"
- "src/backend/**"
- "pyproject.toml"
pull_request: pull_request:
paths:
- "tests/**"
- "src/backend/**"
- "pyproject.toml"
env: env:
POETRY_VERSION: "1.4.0" POETRY_VERSION: "1.4.0"

View file

@ -3,9 +3,17 @@ name: test
on: on:
push: push:
branches: [main] branches: [main]
paths:
- "tests/**"
- "src/backend/**"
- "pyproject.toml"
pull_request: pull_request:
branches: [dev] branches: [dev]
paths:
- "tests/**"
- "src/backend/**"
- "pyproject.toml"
env: env:
POETRY_VERSION: "1.5.0" POETRY_VERSION: "1.5.0"

2
.gitignore vendored
View file

@ -255,3 +255,5 @@ langflow.db
/tmp/* /tmp/*
src/backend/langflow/frontend/ src/backend/langflow/frontend/
.docker .docker
.idea

View file

@ -22,13 +22,19 @@ tests:
@make install_backend @make install_backend
poetry run pytest tests poetry run pytest tests
tests_frontend:
ifeq ($(UI), true)
cd src/frontend && ./run-tests.sh --ui
else
cd src/frontend && ./run-tests.sh
endif
format: format:
poetry run black . poetry run black .
poetry run ruff . --fix poetry run ruff . --fix
cd src/frontend && npm run format cd src/frontend && npm run format
lint: lint:
make install_backend
poetry run mypy src/backend/langflow poetry run mypy src/backend/langflow
poetry run black . --check poetry run black . --check
poetry run ruff . --fix poetry run ruff . --fix
@ -43,19 +49,20 @@ run_frontend:
cd src/frontend && npm start cd src/frontend && npm start
run_cli: run_cli:
poetry run langflow run --path src/frontend/build poetry run langflow --path src/frontend/build
run_cli_debug: run_cli_debug:
poetry run langflow run --path src/frontend/build --log-level debug poetry run langflow --path src/frontend/build --log-level debug
setup_devcontainer: setup_devcontainer:
make init make init
make build_frontend make build_frontend
poetry run langflow --path src/frontend/build @echo 'Run Cli'
make run_cli
frontend: frontend:
make install_frontend @-make install_frontend || (echo "An error occurred while installing frontend dependencies. Attempting to fix." && make install_frontendc)
make run_frontend @make run_frontend
frontendc: frontendc:
make install_frontendc make install_frontendc
@ -66,7 +73,13 @@ install_backend:
backend: backend:
make install_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: build_and_run:
echo 'Removing dist folder' echo 'Removing dist folder'

View file

@ -143,7 +143,7 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
# 🎨 Creating Flows # 🎨 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. Explore by editing prompt parameters, link chains and agents, track an agent's thought process, and export your flow.

View file

@ -218,3 +218,39 @@ Vertex AI is a cloud computing platform offered by Google Cloud Platform (GCP).
- **top_p:** Tokens are selected from most probable to least until the sum of their – defaults to `0.95`. - **top_p:** Tokens are selected from most probable to least until the sum of their – defaults to `0.95`.
- **tuned_model_name:** The name of a tuned model. If provided, model_name is ignored. - **tuned_model_name:** The name of a tuned model. If provided, model_name is ignored.
- **verbose:** This parameter is used to control the level of detail in the output of the chain. When set to True, it will print out some internal states of the chain while it is being run, which can help debug and understand the chain's behavior. If set to False, it will suppress the verbose output – defaults to `False`. - **verbose:** This parameter is used to control the level of detail in the output of the chain. When set to True, it will print out some internal states of the chain while it is being run, which can help debug and understand the chain's behavior. If set to False, it will suppress the verbose output – defaults to `False`.
---
### QianfanLLMEndpoint
Wrapper around [Baidu Qianfan](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) large language models.
:::info
The Qianfan Big Model Platform is a one-stop platform for enterprise developers to develop and operate large models and services. It provides data management based on ERNIE Bot's underlying model (Ernie Bot), automatic model customization and fine-tuning, and one-stop large-scale model customization services for cloud deployment of prediction services, and provides ERNIE Bot's enterprise level service API that can be quickly called, helping to implement the demand for generative AI applications in various industries.
:::
- **Model Name:** Model name. you could get from https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu preset models are mapping to an endpoint. `Model Name` will be ignored if `Endpoint` is set.
- **Qianfan Ak:** which you could get from https://cloud.baidu.com/product/wenxinworkshop.
- **Qianfan Sk:** which you could get from https://cloud.baidu.com/product/wenxinworkshop.
- **Top p:** Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo. The diversity of the output text is affected, and the larger the value, the stronger the diversity of the generated text - defaults to `0.8`.
- **Temperature:** Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo. Higher values make the output more random, while lower values make it more concentrated and deterministic - defaults to `0.95`.
- **Penalty Score:** Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo. By increasing the penalty for generated tokens, the phenomenon of duplicate generation is reduced. A higher value indicates a higher penalty - defaults to `1.0`.
- **Endpoint:** Endpoint of the Qianfan LLM, required if custom model used.
---
### QianfanChatEndpoint
Wrapper around [Baidu Qianfan](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) chat large language models. This component supports some of the LLMs (Large Language Models) available by Baidu qianfan and is used for tasks such as chatbots, Generative Question-Answering (GQA), and summarization.
:::info
The Qianfan Big Model Platform is a one-stop platform for enterprise developers to develop and operate large models and services. It provides data management based on ERNIE Bot's underlying model (Ernie Bot), automatic model customization and fine-tuning, and one-stop large-scale model customization services for cloud deployment of prediction services, and provides ERNIE Bot's enterprise level service API that can be quickly called, helping to implement the demand for generative AI applications in various industries.
:::
- **Model Name:** Model name. you could get from https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu preset models are mapping to an endpoint. `Model Name` will be ignored if `Endpoint` is set.
- **Qianfan Ak:** which you could get from https://cloud.baidu.com/product/wenxinworkshop.
- **Qianfan Sk:** which you could get from https://cloud.baidu.com/product/wenxinworkshop.
- **Top p:** Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo. The diversity of the output text is affected, and the larger the value, the stronger the diversity of the generated text - defaults to `0.8`.
- **Temperature:** Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo. Higher values make the output more random, while lower values make it more concentrated and deterministic - defaults to `0.95`.
- **Penalty Score:** Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo. By increasing the penalty for generated tokens, the phenomenon of duplicate generation is reduced. A higher value indicates a higher penalty - defaults to `1.0`.
- **Endpoint:** Endpoint of the Qianfan LLM, required if custom model used.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Before After
Before After

1281
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@ google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0" google-api-python-client = "^2.79.0"
typer = "^0.9.0" typer = "^0.9.0"
gunicorn = "^21.2.0" gunicorn = "^21.2.0"
langchain = "^0.0.320" langchain = "^0.0.312"
openai = "^0.27.8" openai = "^0.27.8"
pandas = "2.0.3" pandas = "2.0.3"
chromadb = "^0.3.21" chromadb = "^0.3.21"
@ -90,7 +90,11 @@ langfuse = "^1.0.13"
pillow = "^10.0.0" pillow = "^10.0.0"
metal-sdk = "^2.2.0" metal-sdk = "^2.2.0"
markupsafe = "^2.1.3" markupsafe = "^2.1.3"
extract-msg = "^0.45.0"
jq = "^1.6.0"
boto3 = "^1.28.63"
numexpr = "^2.8.6"
qianfan = "0.0.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
types-redis = "^4.6.0.5" types-redis = "^4.6.0.5"

View file

@ -61,6 +61,8 @@ def set_var_for_macos_issue():
import os import os
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES" os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
# https://stackoverflow.com/questions/75747888/uwsgi-segmentation-fault-with-flask-python-app-behind-nginx-after-running-for-2 # noqa
os.environ["no_proxy"] = "*" # to avoid error with gunicorn
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error") logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")

View file

@ -187,14 +187,18 @@ async def stream_build(
valid = False valid = False
update_build_status(cache_service, flow_id, BuildStatus.FAILURE) update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
response = { vertex_id = (
"valid": valid, vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
"params": params, )
"id": vertex.id, if vertex_id in graph.top_level_nodes:
"progress": round(i / number_of_nodes, 2), response = {
} "valid": valid,
"params": params,
"id": vertex_id,
"progress": round(i / number_of_nodes, 2),
}
yield str(StreamData(event="message", data=response)) yield str(StreamData(event="message", data=response))
langchain_object = graph.build() langchain_object = graph.build()
# Now we need to check the input_keys to send them to the client # Now we need to check the input_keys to send them to the client

View file

@ -54,10 +54,9 @@ def get_all(
logger.debug("Building langchain types dict") logger.debug("Building langchain types dict")
try: try:
types_dict = get_all_types_dict(settings_service) return get_all_types_dict(settings_service)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc
return types_dict
# For backwards compatibility we will keep the old endpoint # For backwards compatibility we will keep the old endpoint
@ -70,7 +69,7 @@ def get_all(
"/process/{flow_id}", "/process/{flow_id}",
response_model=ProcessResponse, response_model=ProcessResponse,
) )
async def process_flow( async def process(
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
flow_id: str, flow_id: str,
inputs: Optional[dict] = None, inputs: Optional[dict] = None,

View file

@ -79,4 +79,5 @@ class ConversationalAgent(CustomComponent):
memory=memory, memory=memory,
verbose=True, verbose=True,
return_intermediate_steps=True, return_intermediate_steps=True,
handle_parsing_errors=True,
) )

View file

@ -0,0 +1,232 @@
from langflow import CustomComponent
from langchain.schema import Document
from typing import Any, Dict, List
loaders_info: List[Dict[str, Any]] = [
{
"loader": "AirbyteJSONLoader",
"name": "Airbyte JSON (.jsonl)",
"import": "langchain.document_loaders.AirbyteJSONLoader",
"defaultFor": ["jsonl"],
"allowdTypes": ["jsonl"],
},
{
"loader": "JSONLoader",
"name": "JSON (.json)",
"import": "langchain.document_loaders.JSONLoader",
"defaultFor": ["json"],
"allowdTypes": ["json"],
},
{
"loader": "BSHTMLLoader",
"name": "BeautifulSoup4 HTML (.html, .htm)",
"import": "langchain.document_loaders.BSHTMLLoader",
"allowdTypes": ["html", "htm"],
},
{
"loader": "CSVLoader",
"name": "CSV (.csv)",
"import": "langchain.document_loaders.CSVLoader",
"defaultFor": ["csv"],
"allowdTypes": ["csv"],
},
{
"loader": "CoNLLULoader",
"name": "CoNLL-U (.conllu)",
"import": "langchain.document_loaders.CoNLLULoader",
"defaultFor": ["conllu"],
"allowdTypes": ["conllu"],
},
{
"loader": "EverNoteLoader",
"name": "EverNote (.enex)",
"import": "langchain.document_loaders.EverNoteLoader",
"defaultFor": ["enex"],
"allowdTypes": ["enex"],
},
{
"loader": "FacebookChatLoader",
"name": "Facebook Chat (.json)",
"import": "langchain.document_loaders.FacebookChatLoader",
"allowdTypes": ["json"],
},
{
"loader": "OutlookMessageLoader",
"name": "Outlook Message (.msg)",
"import": "langchain.document_loaders.OutlookMessageLoader",
"defaultFor": ["msg"],
"allowdTypes": ["msg"],
},
{
"loader": "PyPDFLoader",
"name": "PyPDF (.pdf)",
"import": "langchain.document_loaders.PyPDFLoader",
"defaultFor": ["pdf"],
"allowdTypes": ["pdf"],
},
{
"loader": "STRLoader",
"name": "Subtitle (.str)",
"import": "langchain.document_loaders.STRLoader",
"defaultFor": ["str"],
"allowdTypes": ["str"],
},
{
"loader": "TextLoader",
"name": "Text (.txt)",
"import": "langchain.document_loaders.TextLoader",
"defaultFor": ["txt"],
"allowdTypes": ["txt"],
},
{
"loader": "UnstructuredEmailLoader",
"name": "Unstructured Email (.eml)",
"import": "langchain.document_loaders.UnstructuredEmailLoader",
"defaultFor": ["eml"],
"allowdTypes": ["eml"],
},
{
"loader": "UnstructuredHTMLLoader",
"name": "Unstructured HTML (.html, .htm)",
"import": "langchain.document_loaders.UnstructuredHTMLLoader",
"defaultFor": ["html", "htm"],
"allowdTypes": ["html", "htm"],
},
{
"loader": "UnstructuredMarkdownLoader",
"name": "Unstructured Markdown (.md)",
"import": "langchain.document_loaders.UnstructuredMarkdownLoader",
"defaultFor": ["md"],
"allowdTypes": ["md"],
},
{
"loader": "UnstructuredPowerPointLoader",
"name": "Unstructured PowerPoint (.pptx)",
"import": "langchain.document_loaders.UnstructuredPowerPointLoader",
"defaultFor": ["pptx"],
"allowdTypes": ["pptx"],
},
{
"loader": "UnstructuredWordLoader",
"name": "Unstructured Word (.docx)",
"import": "langchain.document_loaders.UnstructuredWordLoader",
"defaultFor": ["docx"],
"allowdTypes": ["docx"],
},
]
class FileLoaderComponent(CustomComponent):
display_name: str = "File Loader"
description: str = "Generic File Loader"
beta = True
def build_config(self):
loader_options = ["Automatic"] + [
loader_info["name"] for loader_info in loaders_info
]
file_types = []
suffixes = []
for loader_info in loaders_info:
if "allowedTypes" in loader_info:
file_types.extend(loader_info["allowedTypes"])
suffixes.extend([f".{ext}" for ext in loader_info["allowedTypes"]])
return {
"file_path": {
"display_name": "File Path",
"required": True,
"field_type": "file",
"file_types": [
"json",
"txt",
"csv",
"jsonl",
"html",
"htm",
"conllu",
"enex",
"msg",
"pdf",
"srt",
"eml",
"md",
"pptx",
"docx",
],
"suffixes": [
".json",
".txt",
".csv",
".jsonl",
".html",
".htm",
".conllu",
".enex",
".msg",
".pdf",
".srt",
".eml",
".md",
".pptx",
".docx",
],
# "file_types" : file_types,
# "suffixes": suffixes,
},
"loader": {
"display_name": "Loader",
"is_list": True,
"required": True,
"options": loader_options,
"value": "Automatic",
},
"code": {"show": False},
}
def build(self, file_path: str, loader: str) -> Document:
file_type = file_path.split(".")[-1]
# Mapeie o nome do loader selecionado para suas informações
selected_loader_info = None
for loader_info in loaders_info:
if loader_info["name"] == loader:
selected_loader_info = loader_info
break
if selected_loader_info is None and loader != "Automatic":
raise ValueError(f"Loader {loader} not found in the loader info list")
if loader == "Automatic":
# Determine o loader automaticamente com base na extensão do arquivo
default_loader_info = None
for info in loaders_info:
if "defaultFor" in info and file_type in info["defaultFor"]:
default_loader_info = info
break
if default_loader_info is None:
raise ValueError(f"No default loader found for file type: {file_type}")
selected_loader_info = default_loader_info
if isinstance(selected_loader_info, dict):
loader_import: str = selected_loader_info["import"]
else:
raise ValueError(
f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}"
)
module_name, class_name = loader_import.rsplit(".", 1)
try:
# Importe o loader dinamicamente
loader_module = __import__(module_name, fromlist=[class_name])
loader_instance = getattr(loader_module, class_name)
except ImportError as e:
raise ValueError(
f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}"
) from e
result = loader_instance(file_path=file_path)
return result.load()

View file

@ -0,0 +1,62 @@
from typing import List
from langflow import CustomComponent
from langchain.document_loaders import AZLyricsLoader
from langchain.document_loaders import CollegeConfidentialLoader
from langchain.document_loaders import GitbookLoader
from langchain.document_loaders import HNLoader
from langchain.document_loaders import IFixitLoader
from langchain.document_loaders import IMSDbLoader
from langchain.document_loaders import WebBaseLoader
from langchain.schema import Document
class UrlLoaderComponent(CustomComponent):
display_name: str = "Url Loader"
description: str = "Generic Url Loader Component"
def build_config(self):
return {
"web_path": {
"display_name": "Url",
"required": True,
},
"loader": {
"display_name": "Loader",
"is_list": True,
"required": True,
"options": [
"AZLyricsLoader",
"CollegeConfidentialLoader",
"GitbookLoader",
"HNLoader",
"IFixitLoader",
"IMSDbLoader",
"WebBaseLoader",
],
"value": "WebBaseLoader",
},
"code": {"show": False},
}
def build(self, web_path: str, loader: str) -> List[Document]:
if loader == "AZLyricsLoader":
loader_instance = AZLyricsLoader(web_path=web_path) # type: ignore
elif loader == "CollegeConfidentialLoader":
loader_instance = CollegeConfidentialLoader(web_path=web_path) # type: ignore
elif loader == "GitbookLoader":
loader_instance = GitbookLoader(web_page=web_path) # type: ignore
elif loader == "HNLoader":
loader_instance = HNLoader(web_path=web_path) # type: ignore
elif loader == "IFixitLoader":
loader_instance = IFixitLoader(web_path=web_path) # type: ignore
elif loader == "IMSDbLoader":
loader_instance = IMSDbLoader(web_path=web_path) # type: ignore
elif loader == "WebBaseLoader":
loader_instance = WebBaseLoader(web_path=web_path) # type: ignore
if loader_instance is None:
raise ValueError(f"No loader found for: {web_path}")
return loader_instance.load()

View file

@ -0,0 +1,45 @@
from typing import Optional
from langflow import CustomComponent
from langchain.llms.bedrock import Bedrock
from langchain.llms.base import BaseLLM
class AmazonBedrockComponent(CustomComponent):
display_name: str = "Amazon Bedrock"
description: str = "LLM model from Amazon Bedrock."
def build_config(self):
return {
"model_id": {
"display_name": "Model Id",
"options": [
"ai21.j2-grande-instruct",
"ai21.j2-jumbo-instruct",
"ai21.j2-mid",
"ai21.j2-mid-v1",
"ai21.j2-ultra",
"ai21.j2-ultra-v1",
"anthropic.claude-instant-v1",
"anthropic.claude-v1",
"anthropic.claude-v2",
"cohere.command-text-v14",
],
},
"credentials_profile_name": {"display_name": "Credentials Profile Name"},
"streaming": {"display_name": "Streaming", "field_type": "bool"},
"code": {"show": False},
}
def build(
self,
model_id: str = "anthropic.claude-instant-v1",
credentials_profile_name: Optional[str] = None,
) -> BaseLLM:
try:
output = Bedrock(
credentials_profile_name=credentials_profile_name,
model_id=model_id,
) # type: ignore
except Exception as e:
raise ValueError("Could not connect to AmazonBedrock API.") from e
return output

View file

@ -0,0 +1,92 @@
from typing import Optional
from langflow import CustomComponent
from langchain.chat_models.baidu_qianfan_endpoint import QianfanChatEndpoint
from langchain.llms.base import BaseLLM
class QianfanChatEndpointComponent(CustomComponent):
display_name: str = "QianfanChatEndpoint"
description: str = (
"Baidu Qianfan chat models. Get more detail from "
"https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint."
)
def build_config(self):
return {
"model": {
"display_name": "Model Name",
"options": [
"ERNIE-Bot",
"ERNIE-Bot-turbo",
"BLOOMZ-7B",
"Llama-2-7b-chat",
"Llama-2-13b-chat",
"Llama-2-70b-chat",
"Qianfan-BLOOMZ-7B-compressed",
"Qianfan-Chinese-Llama-2-7B",
"ChatGLM2-6B-32K",
"AquilaChat-7B",
],
"info": "https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint",
"required": True,
},
"qianfan_ak": {
"display_name": "Qianfan Ak",
"required": True,
"password": True,
"info": "which you could get from https://cloud.baidu.com/product/wenxinworkshop",
},
"qianfan_sk": {
"display_name": "Qianfan Sk",
"required": True,
"password": True,
"info": "which you could get from https://cloud.baidu.com/product/wenxinworkshop",
},
"top_p": {
"display_name": "Top p",
"field_type": "float",
"info": "Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo",
"value": 0.8,
},
"temperature": {
"display_name": "Temperature",
"field_type": "float",
"info": "Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo",
"value": 0.95,
},
"penalty_score": {
"display_name": "Penalty Score",
"field_type": "float",
"info": "Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo",
"value": 1.0,
},
"endpoint": {
"display_name": "Endpoint",
"info": "Endpoint of the Qianfan LLM, required if custom model used.",
},
"code": {"show": False},
}
def build(
self,
model: str = "ERNIE-Bot-turbo",
qianfan_ak: Optional[str] = None,
qianfan_sk: Optional[str] = None,
top_p: Optional[float] = None,
temperature: Optional[float] = None,
penalty_score: Optional[float] = None,
endpoint: Optional[str] = None,
) -> BaseLLM:
try:
output = QianfanChatEndpoint( # type: ignore
model=model,
qianfan_ak=qianfan_ak,
qianfan_sk=qianfan_sk,
top_p=top_p,
temperature=temperature,
penalty_score=penalty_score,
endpoint=endpoint,
)
except Exception as e:
raise ValueError("Could not connect to Baidu Qianfan API.") from e
return output # type: ignore

View file

@ -0,0 +1,92 @@
from typing import Optional
from langflow import CustomComponent
from langchain.llms.baidu_qianfan_endpoint import QianfanLLMEndpoint
from langchain.llms.base import BaseLLM
class QianfanLLMEndpointComponent(CustomComponent):
display_name: str = "QianfanLLMEndpoint"
description: str = (
"Baidu Qianfan hosted open source or customized models. "
"Get more detail from https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint"
)
def build_config(self):
return {
"model": {
"display_name": "Model Name",
"options": [
"ERNIE-Bot",
"ERNIE-Bot-turbo",
"BLOOMZ-7B",
"Llama-2-7b-chat",
"Llama-2-13b-chat",
"Llama-2-70b-chat",
"Qianfan-BLOOMZ-7B-compressed",
"Qianfan-Chinese-Llama-2-7B",
"ChatGLM2-6B-32K",
"AquilaChat-7B",
],
"info": "https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint",
"required": True,
},
"qianfan_ak": {
"display_name": "Qianfan Ak",
"required": True,
"password": True,
"info": "which you could get from https://cloud.baidu.com/product/wenxinworkshop",
},
"qianfan_sk": {
"display_name": "Qianfan Sk",
"required": True,
"password": True,
"info": "which you could get from https://cloud.baidu.com/product/wenxinworkshop",
},
"top_p": {
"display_name": "Top p",
"field_type": "float",
"info": "Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo",
"value": 0.8,
},
"temperature": {
"display_name": "Temperature",
"field_type": "float",
"info": "Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo",
"value": 0.95,
},
"penalty_score": {
"display_name": "Penalty Score",
"field_type": "float",
"info": "Model params, only supported in ERNIE-Bot and ERNIE-Bot-turbo",
"value": 1.0,
},
"endpoint": {
"display_name": "Endpoint",
"info": "Endpoint of the Qianfan LLM, required if custom model used.",
},
"code": {"show": False},
}
def build(
self,
model: str = "ERNIE-Bot-turbo",
qianfan_ak: Optional[str] = None,
qianfan_sk: Optional[str] = None,
top_p: Optional[float] = None,
temperature: Optional[float] = None,
penalty_score: Optional[float] = None,
endpoint: Optional[str] = None,
) -> BaseLLM:
try:
output = QianfanLLMEndpoint( # type: ignore
model=model,
qianfan_ak=qianfan_ak,
qianfan_sk=qianfan_sk,
top_p=top_p,
temperature=temperature,
penalty_score=penalty_score,
endpoint=endpoint,
)
except Exception as e:
raise ValueError("Could not connect to Baidu Qianfan API.") from e
return output # type: ignore

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from langflow import CustomComponent from langflow import CustomComponent
from langchain.llms import HuggingFaceEndpoint from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
from langchain.llms.base import BaseLLM from langchain.llms.base import BaseLLM
@ -13,7 +13,6 @@ class HuggingFaceEndpointsComponent(CustomComponent):
"endpoint_url": {"display_name": "Endpoint URL", "password": True}, "endpoint_url": {"display_name": "Endpoint URL", "password": True},
"task": { "task": {
"display_name": "Task", "display_name": "Task",
"type": "select",
"options": ["text2text-generation", "text-generation", "summarization"], "options": ["text2text-generation", "text-generation", "summarization"],
}, },
"huggingfacehub_api_token": {"display_name": "API token", "password": True}, "huggingfacehub_api_token": {"display_name": "API token", "password": True},
@ -27,7 +26,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
def build( def build(
self, self,
endpoint_url: str, endpoint_url: str,
task="text2text-generation", task: str = "text2text-generation",
huggingfacehub_api_token: Optional[str] = None, huggingfacehub_api_token: Optional[str] = None,
model_kwargs: Optional[dict] = None, model_kwargs: Optional[dict] = None,
) -> BaseLLM: ) -> BaseLLM:
@ -36,6 +35,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
endpoint_url=endpoint_url, endpoint_url=endpoint_url,
task=task, task=task,
huggingfacehub_api_token=huggingfacehub_api_token, huggingfacehub_api_token=huggingfacehub_api_token,
model_kwargs=model_kwargs,
) )
except Exception as e: except Exception as e:
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e raise ValueError("Could not connect to HuggingFace Endpoints API.") from e

View file

@ -0,0 +1,48 @@
from typing import Optional
from langflow import CustomComponent
from langchain.retrievers import AmazonKendraRetriever
from langchain.schema import BaseRetriever
class AmazonKendraRetrieverComponent(CustomComponent):
display_name: str = "Amazon Kendra Retriever"
description: str = "Retriever that uses the Amazon Kendra API."
def build_config(self):
return {
"index_id": {"display_name": "Index ID"},
"region_name": {"display_name": "Region Name"},
"credentials_profile_name": {"display_name": "Credentials Profile Name"},
"attribute_filter": {
"display_name": "Attribute Filter",
"field_type": "code",
},
"top_k": {"display_name": "Top K", "field_type": "int"},
"user_context": {
"display_name": "User Context",
"field_type": "code",
},
"code": {"show": False},
}
def build(
self,
index_id: str,
top_k: int = 3,
region_name: Optional[str] = None,
credentials_profile_name: Optional[str] = None,
attribute_filter: Optional[dict] = None,
user_context: Optional[dict] = None,
) -> BaseRetriever:
try:
output = AmazonKendraRetriever(
index_id=index_id,
top_k=top_k,
region_name=region_name,
credentials_profile_name=credentials_profile_name,
attribute_filter=attribute_filter,
user_context=user_context,
) # type: ignore
except Exception as e:
raise ValueError("Could not connect to AmazonKendra API.") from e
return output

View file

@ -14,7 +14,7 @@ class ChromaComponent(CustomComponent):
A custom component for implementing a Vector Store using Chroma. A custom component for implementing a Vector Store using Chroma.
""" """
display_name: str = "Chroma (Custom Component)" display_name: str = "Chroma"
description: str = "Implementation of Vector Store using Chroma" description: str = "Implementation of Vector Store using Chroma"
documentation = "https://python.langchain.com/docs/integrations/vectorstores/chroma" documentation = "https://python.langchain.com/docs/integrations/vectorstores/chroma"
beta = True beta = True

View file

@ -5,7 +5,6 @@ from langchain.vectorstores import Vectara
from langchain.schema import Document from langchain.schema import Document
from langchain.vectorstores.base import VectorStore from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseRetriever from langchain.schema import BaseRetriever
from langchain.embeddings.base import Embeddings
class VectaraComponent(CustomComponent): class VectaraComponent(CustomComponent):
@ -22,7 +21,6 @@ class VectaraComponent(CustomComponent):
"vectara_api_key": {"display_name": "Vectara API Key", "password": True}, "vectara_api_key": {"display_name": "Vectara API Key", "password": True},
"code": {"show": False}, "code": {"show": False},
"documents": {"display_name": "Documents"}, "documents": {"display_name": "Documents"},
"embedding": {"display_name": "Embedding"},
} }
def build( def build(
@ -30,21 +28,21 @@ class VectaraComponent(CustomComponent):
vectara_customer_id: str, vectara_customer_id: str,
vectara_corpus_id: str, vectara_corpus_id: str,
vectara_api_key: str, vectara_api_key: str,
embedding: Optional[Embeddings] = None,
documents: Optional[Document] = None, documents: Optional[Document] = None,
) -> Union[VectorStore, BaseRetriever]: ) -> Union[VectorStore, BaseRetriever]:
# If documents, then we need to create a Vectara instance using .from_documents # If documents, then we need to create a Vectara instance using .from_documents
if documents is not None and embedding is not None: if documents is not None:
return Vectara.from_documents( return Vectara.from_documents(
documents=documents, # type: ignore documents=documents, # type: ignore
vectara_customer_id=vectara_customer_id, vectara_customer_id=vectara_customer_id,
vectara_corpus_id=vectara_corpus_id, vectara_corpus_id=vectara_corpus_id,
vectara_api_key=vectara_api_key, vectara_api_key=vectara_api_key,
embedding=embedding, source="langflow",
) )
return Vectara( return Vectara(
vectara_customer_id=vectara_customer_id, vectara_customer_id=vectara_customer_id,
vectara_corpus_id=vectara_corpus_id, vectara_corpus_id=vectara_corpus_id,
vectara_api_key=vectara_api_key, vectara_api_key=vectara_api_key,
source="langflow",
) )

View file

@ -265,8 +265,8 @@ retrievers:
# ZepRetriever: # ZepRetriever:
# documentation: "https://python.langchain.com/docs/modules/data_connection/retrievers/integrations/zep_memorystore" # documentation: "https://python.langchain.com/docs/modules/data_connection/retrievers/integrations/zep_memorystore"
vectorstores: vectorstores:
Chroma: # Chroma:
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/chroma" # documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/chroma"
Qdrant: Qdrant:
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/qdrant" documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/qdrant"
Weaviate: Weaviate:

View file

@ -1,3 +1,53 @@
from .base import NestedDict # LANGCHAIN_BASE_TYPES = {
# "Chain": Chain,
# "AgentExecutor": AgentExecutor,
# "Tool": Tool,
# "BaseLLM": BaseLLM,
# "PromptTemplate": PromptTemplate,
# "BaseLoader": BaseLoader,
# "Document": Document,
# "TextSplitter": TextSplitter,
# "VectorStore": VectorStore,
# "Embeddings": Embeddings,
# "BaseRetriever": BaseRetriever,
# "BaseOutputParser": BaseOutputParser,
# "BaseMemory": BaseMemory,
# "BaseChatMemory": BaseChatMemory,
# }
from .constants import (
Tool,
PromptTemplate,
Chain,
BaseChatMemory,
BaseLLM,
BaseLoader,
BaseMemory,
BaseOutputParser,
BaseRetriever,
VectorStore,
Embeddings,
TextSplitter,
Document,
AgentExecutor,
NestedDict,
Data,
)
__all__ = ["NestedDict"] __all__ = [
"NestedDict",
"Data",
"Tool",
"PromptTemplate",
"Chain",
"BaseChatMemory",
"BaseLLM",
"BaseLoader",
"BaseMemory",
"BaseOutputParser",
"BaseRetriever",
"VectorStore",
"Embeddings",
"TextSplitter",
"Document",
"AgentExecutor",
]

View file

@ -1,4 +0,0 @@
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]

View file

@ -0,0 +1,50 @@
from langchain.agents.agent import AgentExecutor
from langchain.chains.base import Chain
from langchain.document_loaders.base import BaseLoader
from langchain.llms.base import BaseLLM
from langchain.memory.chat_memory import BaseChatMemory
from langchain.prompts import PromptTemplate
from langchain.schema import BaseOutputParser, BaseRetriever, Document
from langchain.schema.embeddings import Embeddings
from langchain.schema.memory import BaseMemory
from langchain.text_splitter import TextSplitter
from langchain.tools import Tool
from langchain.vectorstores.base import VectorStore
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]
class Data:
pass
LANGCHAIN_BASE_TYPES = {
"Chain": Chain,
"AgentExecutor": AgentExecutor,
"Tool": Tool,
"BaseLLM": BaseLLM,
"PromptTemplate": PromptTemplate,
"BaseLoader": BaseLoader,
"Document": Document,
"TextSplitter": TextSplitter,
"VectorStore": VectorStore,
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
"BaseOutputParser": BaseOutputParser,
"BaseMemory": BaseMemory,
"BaseChatMemory": BaseChatMemory,
}
# Langchain base types plus Python base types
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
**LANGCHAIN_BASE_TYPES,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
"NestedDict": NestedDict,
"Data": Data,
}

View file

@ -1,28 +1,79 @@
from loguru import logger from loguru import logger
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pydantic import BaseModel, Field
from typing import List, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
class SourceHandle(BaseModel):
baseClasses: List[str] = Field(
..., description="List of base classes for the source handle."
)
dataType: str = Field(..., description="Data type for the source handle.")
id: str = Field(..., description="Unique identifier for the source handle.")
class TargetHandle(BaseModel):
fieldName: str = Field(..., description="Field name for the target handle.")
id: str = Field(..., description="Unique identifier for the target handle.")
inputTypes: Optional[List[str]] = Field(
None, description="List of input types for the target handle."
)
type: str = Field(..., description="Type of the target handle.")
class Edge: class Edge:
def __init__(self, source: "Vertex", target: "Vertex", edge: dict): def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
self.source: "Vertex" = source self.source: "Vertex" = source
self.target: "Vertex" = target self.target: "Vertex" = target
self.source_handle = edge.get("sourceHandle", "") if data := edge.get("data", {}):
self.target_handle = edge.get("targetHandle", "") self._source_handle = data.get("sourceHandle", {})
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD' self._target_handle = data.get("targetHandle", {})
# target_param is documents self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
self.target_param = self.target_handle.split("|")[1] self.target_handle: TargetHandle = TargetHandle(**self._target_handle)
self.target_param = self.target_handle.fieldName
# validate handles
self.validate_handles()
else:
# Logging here because this is a breaking change
logger.error("Edge data is empty")
self._source_handle = edge.get("sourceHandle", "")
self._target_handle = edge.get("targetHandle", "")
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
# target_param is documents
self.target_param = self._target_handle.split("|")[1]
# Validate in __init__ to fail fast
self.validate_edge() self.validate_edge()
def validate_handles(self) -> None:
if self.target_handle.inputTypes is None:
self.valid_handles = (
self.target_handle.type in self.source_handle.baseClasses
)
else:
self.valid_handles = (
any(
baseClass in self.target_handle.inputTypes
for baseClass in self.source_handle.baseClasses
)
or self.target_handle.type in self.source_handle.baseClasses
)
if not self.valid_handles:
logger.debug(self.source_handle)
logger.debug(self.target_handle)
raise ValueError(
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
f"has invalid handles"
)
def __setstate__(self, state): def __setstate__(self, state):
self.source = state["source"] self.source = state["source"]
self.target = state["target"] self.target = state["target"]
self.target_param = state["target_param"] self.target_param = state["target_param"]
self.source_handle = state["source_handle"] self.source_handle = state.get("source_handle")
self.target_handle = state["target_handle"] self.target_handle = state.get("target_handle")
def reset(self) -> None: def reset(self) -> None:
self.source._build_params() self.source._build_params()

View file

@ -2,6 +2,7 @@ from typing import Dict, Generator, List, Type, Union
from langflow.graph.edge.base import Edge from langflow.graph.edge.base import Edge
from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.graph.constants import lazy_load_vertex_dict
from langflow.graph.graph.utils import process_flow
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.types import ( from langflow.graph.vertex.types import (
FileToolVertex, FileToolVertex,
@ -19,11 +20,21 @@ class Graph:
def __init__( def __init__(
self, self,
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]], nodes: List[Dict],
edges: List[Dict[str, str]], edges: List[Dict[str, str]],
) -> None: ) -> None:
self._nodes = nodes self._nodes = nodes
self._edges = edges self._edges = edges
self.raw_graph_data = {"nodes": nodes, "edges": edges}
self.top_level_nodes = []
for node in self._nodes:
if node_id := node.get("id"):
self.top_level_nodes.append(node_id)
self._graph_data = process_flow(self.raw_graph_data)
self._nodes = self._graph_data["nodes"]
self._edges = self._graph_data["edges"]
self._build_graph() self._build_graph()
def __setstate__(self, state): def __setstate__(self, state):
@ -50,6 +61,7 @@ class Graph:
edges = payload["edges"] edges = payload["edges"]
return cls(nodes, edges) return cls(nodes, edges)
except KeyError as exc: except KeyError as exc:
logger.exception(exc)
raise ValueError( raise ValueError(
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}" f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
) from exc ) from exc
@ -215,7 +227,9 @@ class Graph:
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
VertexClass = self._get_vertex_class(node_type, node_lc_type) VertexClass = self._get_vertex_class(node_type, node_lc_type)
nodes.append(VertexClass(node)) vertex = VertexClass(node)
vertex.set_top_level(self.top_level_nodes)
nodes.append(vertex)
return nodes return nodes

View file

@ -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

View file

@ -38,6 +38,8 @@ class Vertex:
self.task_id: Optional[str] = None self.task_id: Optional[str] = None
self.is_task = is_task self.is_task = is_task
self.params = params or {} self.params = params or {}
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
self.parent_is_top_level = False
def reset_params(self): def reset_params(self):
for edge in self.edges: for edge in self.edges:
@ -88,6 +90,11 @@ class Vertex:
self._built = False self._built = False
self.artifacts: Dict[str, Any] = {} self.artifacts: Dict[str, Any] = {}
self.task_id: Optional[str] = None self.task_id: Optional[str] = None
self.parent_node_id = state["parent_node_id"]
self.parent_is_top_level = state["parent_is_top_level"]
def set_top_level(self, top_level_nodes: List[str]) -> None:
self.parent_is_top_level = self.parent_node_id in top_level_nodes
def _parse_data(self) -> None: def _parse_data(self) -> None:
self.data = self._data["data"] self.data = self._data["data"]
@ -209,6 +216,16 @@ class Vertex:
} }
elif isinstance(_value, dict): elif isinstance(_value, dict):
params[key] = _value params[key] = _value
elif value.get("type") == "int" and value.get("value") is not None:
try:
params[key] = int(value.get("value"))
except ValueError:
params[key] = value.get("value")
elif value.get("type") == "float" and value.get("value") is not None:
try:
params[key] = float(value.get("value"))
except ValueError:
params[key] = value.get("value")
else: else:
params[key] = value.get("value") params[key] = value.get("value")
@ -342,7 +359,7 @@ class Vertex:
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.exception(exc)
raise ValueError( raise ValueError(
f"Error building node {self.vertex_type}: {str(exc)}" f"Error building node {self.vertex_type}(ID:{self.id}): {str(exc)}"
) from exc ) from exc
def _update_built_object_and_artifacts(self, result): def _update_built_object_and_artifacts(self, result):

View file

@ -1,65 +1,33 @@
from langchain.prompts import PromptTemplate
from langchain.chains.base import Chain
from langchain.document_loaders.base import BaseLoader
from langchain.schema.embeddings import Embeddings
from langchain.llms.base import BaseLLM
from langchain.schema import BaseRetriever, Document
from langchain.text_splitter import TextSplitter
from langchain.tools import Tool
from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseOutputParser
from langchain.schema.memory import BaseMemory
from langchain.memory.chat_memory import BaseChatMemory
from langchain.agents.agent import AgentExecutor
LANGCHAIN_BASE_TYPES = {
"Chain": Chain,
"AgentExecutor": AgentExecutor,
"Tool": Tool,
"BaseLLM": BaseLLM,
"PromptTemplate": PromptTemplate,
"BaseLoader": BaseLoader,
"Document": Document,
"TextSplitter": TextSplitter,
"VectorStore": VectorStore,
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
"BaseOutputParser": BaseOutputParser,
"BaseMemory": BaseMemory,
"BaseChatMemory": BaseChatMemory,
}
# Langchain base types plus Python base types
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
**LANGCHAIN_BASE_TYPES,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
}
DEFAULT_CUSTOM_COMPONENT_CODE = """from langflow import CustomComponent DEFAULT_CUSTOM_COMPONENT_CODE = """from langflow import CustomComponent
from langchain.llms.base import BaseLLM from langflow.field_typing import (
from langchain.chains import LLMChain Tool,
from langchain.prompts import PromptTemplate PromptTemplate,
from langchain.schema import Document Chain,
BaseChatMemory,
BaseLLM,
BaseLoader,
BaseMemory,
BaseOutputParser,
BaseRetriever,
VectorStore,
Embeddings,
TextSplitter,
Document,
AgentExecutor,
NestedDict,
Data,
)
import requests
class YourComponent(CustomComponent): class Component(CustomComponent):
display_name: str = "Custom Component" display_name: str = "Custom Component"
description: str = "Create any custom component you want!" description: str = "Create any custom component you want!"
def build_config(self): def build_config(self):
return { "url": { "multiline": True, "required": True } } return {"param": {"display_name": "Parameter"}}
def build(self, param: Data) -> Data:
return param
def build(self, url: str, llm: BaseLLM, prompt: PromptTemplate) -> Document:
response = requests.get(url)
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(response.text[:300])
return Document(page_content=str(result))
""" """

View file

@ -1,7 +1,7 @@
from typing import Any, Callable, List, Optional, Union from typing import Any, Callable, List, Optional, Union
from uuid import UUID from uuid import UUID
from fastapi import HTTPException from fastapi import HTTPException
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.component import Component from langflow.interface.custom.component import Component
from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.directory_reader import DirectoryReader
from langflow.services.getters import get_db_service from langflow.services.getters import get_db_service
@ -108,6 +108,9 @@ class CustomComponent(Component, extra=Extra.allow):
), ),
}, },
) )
elif not arg.get("type"):
# Set the type to Data
arg["type"] = "Data"
return args return args
@property @property

View file

@ -5,7 +5,6 @@ from langchain.agents.load_tools import (
_EXTRA_OPTIONAL_TOOLS, _EXTRA_OPTIONAL_TOOLS,
_LLM_TOOLS, _LLM_TOOLS,
) )
from langchain.tools.python.tool import PythonInputs
from langflow.custom import customs from langflow.custom import customs
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
@ -162,14 +161,8 @@ class ToolCreator(LangChainTypeCreator):
template = Template(fields=fields, type_name=tool_type) template = Template(fields=fields, type_name=tool_type)
tool_params = {**tool_params, **self.type_to_loader_dict[name]["params"]} tool_params = {**tool_params, **self.type_to_loader_dict[name]["params"]}
template_dict = template.to_dict()
if (
"args_schema" in template_dict
and template_dict.get("args_schema").get("value") == PythonInputs
):
template_dict["args_schema"]["value"] = ""
return { return {
"template": util.format_dict(template_dict), "template": util.format_dict(template.to_dict()),
**tool_params, **tool_params,
"base_classes": base_classes, "base_classes": base_classes,
} }

View file

@ -4,7 +4,7 @@ from typing import Any, List
from langflow.api.utils import get_new_key from langflow.api.utils import get_new_key
from langflow.interface.agents.base import agent_creator from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.utils import extract_inner_type from langflow.interface.custom.utils import extract_inner_type
from langflow.interface.document_loaders.base import documentloader_creator from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator from langflow.interface.embeddings.base import embedding_creator
@ -288,6 +288,24 @@ def add_base_classes(frontend_node, return_types: List[str]):
frontend_node.get("base_classes").append(base_class) frontend_node.get("base_classes").append(base_class)
def add_output_types(frontend_node, return_types: List[str]):
"""Add output types to the frontend node"""
for return_type in return_types:
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
raise HTTPException(
status_code=400,
detail={
"error": (
"Invalid return type should be one of: "
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
),
"traceback": traceback.format_exc(),
},
)
frontend_node.get("output_types").append(return_type)
def build_langchain_template_custom_component(custom_component: CustomComponent): def build_langchain_template_custom_component(custom_component: CustomComponent):
"""Build a custom component template for the langchain""" """Build a custom component template for the langchain"""
try: try:
@ -314,6 +332,9 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
add_base_classes( add_base_classes(
frontend_node, custom_component.get_function_entrypoint_return_type frontend_node, custom_component.get_function_entrypoint_return_type
) )
add_output_types(
frontend_node, custom_component.get_function_entrypoint_return_type
)
logger.debug("Added base classes") logger.debug("Added base classes")
return frontend_node return frontend_node
except Exception as exc: except Exception as exc:

View file

@ -28,14 +28,10 @@ class UtilityCreator(LangChainTypeCreator):
""" """
if self.type_dict is None: if self.type_dict is None:
settings_service = get_settings_service() settings_service = get_settings_service()
self.type_dict = {} self.type_dict = {
for utility_name in utilities.__all__: utility_name: import_class(f"langchain.utilities.{utility_name}")
try: for utility_name in utilities.__all__
imported = import_class(f"langchain.utilities.{utility_name}") }
self.type_dict[utility_name] = imported
except Exception:
pass
self.type_dict["SQLDatabase"] = utilities.SQLDatabase self.type_dict["SQLDatabase"] = utilities.SQLDatabase
# Filter according to settings.utilities # Filter according to settings.utilities
self.type_dict = { self.type_dict = {

View file

@ -34,7 +34,9 @@ def get_langfuse_callback(trace_id):
if langfuse := LangfuseInstance.get(): if langfuse := LangfuseInstance.get():
logger.debug("Langfuse credentials found") logger.debug("Langfuse credentials found")
try: try:
trace = langfuse.trace(CreateTrace(id=trace_id)) trace = langfuse.trace(
CreateTrace(name="langflow-" + trace_id, id=trace_id)
)
return trace.getNewHandler() return trace.getNewHandler()
except Exception as exc: except Exception as exc:
logger.error(f"Error initializing langfuse callback: {exc}") logger.error(f"Error initializing langfuse callback: {exc}")

View file

@ -32,7 +32,11 @@ def initialize_database():
try: try:
database_service.run_migrations() database_service.run_migrations()
except CommandError as exc: 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 raise exc
# This means there's wrong revision in the DB # This means there's wrong revision in the DB
# We need to delete the alembic_version table # We need to delete the alembic_version table

View file

@ -44,7 +44,7 @@ class FieldFormatters(BaseModel):
class FrontendNode(BaseModel): class FrontendNode(BaseModel):
template: Template template: Template
description: str description: Optional[str] = None
base_classes: List[str] base_classes: List[str]
name: str = "" name: str = ""
display_name: str = "" display_name: str = ""

View file

@ -2,6 +2,7 @@ from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template from langflow.template.template.base import Template
from langflow.interface.custom.constants import DEFAULT_CUSTOM_COMPONENT_CODE from langflow.interface.custom.constants import DEFAULT_CUSTOM_COMPONENT_CODE
from typing import Optional
class CustomComponentFrontendNode(FrontendNode): class CustomComponentFrontendNode(FrontendNode):
@ -24,7 +25,7 @@ class CustomComponentFrontendNode(FrontendNode):
) )
], ],
) )
description: str = "Create any custom component you want!" description: Optional[str] = None
base_classes: list[str] = [] base_classes: list[str] = []
def to_dict(self): def to_dict(self):

View file

@ -44,6 +44,7 @@ class PromptFrontendNode(FrontendNode):
# All prompt fields should be password=False # All prompt fields should be password=False
field.password = False field.password = False
field.dynamic = True
class PromptTemplateNode(FrontendNode): class PromptTemplateNode(FrontendNode):

View file

@ -191,7 +191,9 @@ def get_base_classes(cls):
"""Get the base classes of a class. """Get the base classes of a class.
These are used to determine the output of the nodes. These are used to determine the output of the nodes.
""" """
if bases := cls.__bases__:
if hasattr(cls, "__bases__") and cls.__bases__:
bases = cls.__bases__
result = [] result = []
for base in bases: for base in bases:
if any(type in base.__module__ for type in ["pydantic", "abc"]): if any(type in base.__module__ for type in ["pydantic", "abc"]):

View file

@ -22,5 +22,5 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
/test-results/ /test-results/
/playwright-report/ /playwright-report/*/
/playwright/.cache/ /playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View 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>

View file

@ -20,11 +20,13 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html", reporter: [
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000', // baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry", trace: "on-first-retry",
@ -69,9 +71,16 @@ export default defineConfig({
], ],
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
// webServer: { // webServer: [
// command: 'npm run start', // {
// url: 'http://127.0.0.1:3000', // command: "npm run backend",
// reuseExistingServer: !process.env.CI, // reuseExistingServer: !process.env.CI,
// }, // timeout: 120 * 1000,
// },
// {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI,
// },
// ],
}); });

78
src/frontend/run-tests.sh Executable file
View file

@ -0,0 +1,78 @@
#!/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
# install playwright if there is not installed yet
npx playwright install
# 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 25
# 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

View file

@ -16,8 +16,8 @@ import {
FETCH_ERROR_MESSAGE, FETCH_ERROR_MESSAGE,
} from "./constants/constants"; } from "./constants/constants";
import { alertContext } from "./contexts/alertContext"; import { alertContext } from "./contexts/alertContext";
import { FlowsContext } from "./contexts/flowsContext";
import { locationContext } from "./contexts/locationContext"; import { locationContext } from "./contexts/locationContext";
import { TabsContext } from "./contexts/tabsContext";
import { typesContext } from "./contexts/typesContext"; import { typesContext } from "./contexts/typesContext";
import Router from "./routes"; import Router from "./routes";
@ -30,7 +30,7 @@ export default function App() {
setShowSideBar(true); setShowSideBar(true);
setIsStackedOpen(true); setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]); }, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext); const { hardReset } = useContext(FlowsContext);
const { const {
errorData, errorData,

View file

@ -23,15 +23,15 @@ import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { TOOLTIP_EMPTY } from "../../../../constants/constants"; import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components"; import { ParameterComponentType } from "../../../../types/components";
import { TabsState } from "../../../../types/tabs";
import { import {
convertObjToArray, convertObjToArray,
convertValuesToNumbers, convertValuesToNumbers,
hasDuplicateKeys, hasDuplicateKeys,
isValidConnection, isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { import {
nodeColors, nodeColors,
@ -53,14 +53,16 @@ export default function ParameterComponent({
required = false, required = false,
optionalHandle = null, optionalHandle = null,
info = "", info = "",
proxy,
showNode, showNode,
index = "",
}: ParameterComponentType): JSX.Element { }: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null); const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null); const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const { setTabsState, tabId, flows } = useContext(TabsContext); const { setTabsState, tabId, flows } = useContext(FlowsContext);
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null; const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
@ -80,8 +82,9 @@ export default function ParameterComponent({
const { reactFlowInstance, setFilterEdge } = useContext(typesContext); const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled = let disabled =
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ?? reactFlowInstance
false; ?.getEdges()
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
const { data: myData } = useContext(typesContext); const { data: myData } = useContext(typesContext);
@ -112,7 +115,6 @@ export default function ParameterComponent({
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => { useEffect(() => {
if (name === "openai_api_base") console.log(info);
// @ts-ignore // @ts-ignore
infoHtml.current = ( infoHtml.current = (
<div className="h-full w-full break-words"> <div className="h-full w-full break-words">
@ -210,32 +212,40 @@ export default function ParameterComponent({
!optionalHandle ? ( !optionalHandle ? (
<></> <></>
) : ( ) : (
<ShadTooltip <Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
styleClasses={"tooltip-fixed-width custom-scroll nowheel"} <div className="flex">
delayDuration={0} <ShadTooltip
content={refHtml.current} styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
side={left ? "left" : "right"} delayDuration={0}
> content={refHtml.current}
<Handle side={left ? "left" : "right"}
type={left ? "target" : "source"} >
position={left ? Position.Left : Position.Right} <Handle
id={id} type={left ? "target" : "source"}
isValidConnection={(connection) => position={left ? Position.Left : Position.Right}
isValidConnection(connection, reactFlowInstance!) id={
} proxy
className={classNames( ? scapedJSONStringfy({ ...id, proxy })
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ", : scapedJSONStringfy(id)
"h-3 w-3 rounded-full border-2 bg-background" }
)} isValidConnection={(connection) =>
style={{ isValidConnection(connection, reactFlowInstance!)
borderColor: color, }
top: position, className={classNames(
}} left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
onClick={() => { "h-3 w-3 rounded-full border-2 bg-background"
setFilterEdge(groupedEdge.current); )}
}} style={{
></Handle> borderColor: color,
</ShadTooltip> top: position,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
) )
) : ( ) : (
<div <div
@ -250,7 +260,13 @@ export default function ParameterComponent({
(info !== "" ? " flex items-center" : "") (info !== "" ? " flex items-center" : "")
} }
> >
{title} {proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className="text-status-red">{required ? " *" : ""}</span> <span className="text-status-red">{required ? " *" : ""}</span>
<div className=""> <div className="">
{info !== "" && ( {info !== "" && (
@ -290,7 +306,11 @@ export default function ParameterComponent({
<Handle <Handle
type={left ? "target" : "source"} type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right} position={left ? Position.Left : Position.Right}
id={id} id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) => isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!) isValidConnection(connection, reactFlowInstance!)
} }
@ -331,9 +351,11 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"textarea-" + index}
/> />
) : ( ) : (
<InputComponent <InputComponent
id={"input-" + index}
disabled={disabled} disabled={disabled}
password={data.node?.template[name].password ?? false} password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
@ -344,6 +366,7 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? ( ) : left === true && type === "bool" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<ToggleShadComponent <ToggleShadComponent
id={"toggle-" + index}
disabled={disabled} disabled={disabled}
enabled={data.node?.template[name].value ?? false} enabled={data.node?.template[name].value ?? false}
setEnabled={(isEnabled) => { setEnabled={(isEnabled) => {
@ -373,6 +396,11 @@ export default function ParameterComponent({
) : left === true && type === "code" ? ( ) : left === true && type === "code" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<CodeAreaComponent <CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false} dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={(nodeClass) => { setNodeClass={(nodeClass) => {
data.node = nodeClass; data.node = nodeClass;
@ -381,6 +409,7 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"code-input-" + index}
/> />
</div> </div>
) : left === true && type === "file" ? ( ) : left === true && type === "file" ? (
@ -402,11 +431,13 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"int-input-" + index}
/> />
</div> </div>
) : left === true && type === "prompt" ? ( ) : left === true && type === "prompt" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<PromptAreaComponent <PromptAreaComponent
readonly={data.node?.flow ? true : false}
field_name={name} field_name={name}
setNodeClass={(nodeClass) => { setNodeClass={(nodeClass) => {
data.node = nodeClass; data.node = nodeClass;
@ -420,6 +451,7 @@ export default function ParameterComponent({
onChange={(e) => { onChange={(e) => {
handleOnNewValue(e); handleOnNewValue(e);
}} }}
id={"prompt-input-" + index}
/> />
</div> </div>
) : left === true && type === "NestedDict" ? ( ) : left === true && type === "NestedDict" ? (

View file

@ -4,30 +4,46 @@ import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent"; import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { useSSE } from "../../contexts/SSEContext"; import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components"; import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { cleanEdges } from "../../utils/reactflowUtils"; import {
cleanEdges,
handleKeyDown,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils"; import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils"; import { classNames, getFieldTitle } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({ export default function GenericNode({
data: olddata, data: olddata,
xPos,
yPos,
selected, selected,
}: { }: {
data: NodeDataType; data: NodeDataType;
selected: boolean; selected: boolean;
xPos: number;
yPos: number;
}): JSX.Element { }): JSX.Element {
const [data, setData] = useState(olddata); const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(TabsContext); const { updateFlow, flows, tabId } = useContext(FlowsContext);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } = const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext); useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(true);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true); const [showNode, setShowNode] = useState<boolean>(true);
@ -111,6 +127,7 @@ export default function GenericNode({
<> <>
<NodeToolbar> <NodeToolbar>
<NodeToolbarComponent <NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data} data={data}
setData={setData} setData={setData}
deleteNode={deleteNode} deleteNode={deleteNode}
@ -151,7 +168,7 @@ export default function GenericNode({
} }
> >
<IconComponent <IconComponent
name={name} name={data.node?.flow ? "Ungroup" : name}
className={ className={
"generic-node-icon " + "generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12") (!showNode && "absolute inset-x-6 h-12 w-12")
@ -160,11 +177,35 @@ export default function GenericNode({
/> />
{showNode && ( {showNode && (
<div className="generic-node-tooltip-div"> <div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}> {data.node?.flow && inputName ? (
<div className="generic-node-tooltip-div text-primary"> <div>
{data.node?.display_name} <InputComponent
autoFocus
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
/>
</div> </div>
</ShadTooltip> ) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="generic-node-tooltip-div text-primary"
onDoubleClick={() => setInputName(true)}
>
{data.node?.display_name}
</div>
</ShadTooltip>
)}
</div> </div>
)} )}
</div> </div>
@ -178,16 +219,15 @@ export default function GenericNode({
data.node!.template[templateField].show && data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && ( !data.node!.template[templateField].advanced && (
<ParameterComponent <ParameterComponent
key={ index={idx.toString()}
(data.node!.template[ key={scapedJSONStringfy({
templateField inputTypes:
].input_types?.join(";") ?? data.node!.template[templateField].input_types,
data.node!.template[templateField].type) + type: data.node!.template[templateField].type,
"|" + id: data.id,
templateField + fieldName: templateField,
"|" + proxy: data.node!.template[templateField].proxy,
data.id })}
}
data={data} data={data}
setData={setData} setData={setData}
color={ color={
@ -199,15 +239,10 @@ export default function GenericNode({
] ?? ] ??
nodeColors.unknown nodeColors.unknown
} }
title={ title={getFieldTitle(
data.node?.template[templateField].display_name data.node?.template!,
? data.node.template[templateField].display_name templateField
: data.node?.template[templateField].name )}
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
tooltipTitle={ tooltipTitle={
@ -217,31 +252,31 @@ export default function GenericNode({
data.node?.template[templateField].type data.node?.template[templateField].type
} }
required={ required={
data.node?.template[templateField].required data.node!.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
} }
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true} left={true}
type={data.node?.template[templateField].type} type={data.node?.template[templateField].type}
optionalHandle={ optionalHandle={
data.node?.template[templateField].input_types data.node?.template[templateField].input_types
} }
proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
/> />
) )
)} )}
<ParameterComponent <ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join( key={scapedJSONStringfy({
"|" baseClasses: data.node!.base_classes,
)} id: data.id,
dataType: data.type,
})}
data={data} data={data}
setData={setData} setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown} color={nodeColors[types[data.type]] ?? nodeColors.unknown}
@ -252,9 +287,11 @@ export default function GenericNode({
: data.type : data.type
} }
tooltipTitle={data.node?.base_classes.join("\n")} tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join( id={{
"|" baseClasses: data.node!.base_classes,
)} id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode} showNode={showNode}
@ -330,33 +367,71 @@ export default function GenericNode({
className={ className={
showNode showNode
? "generic-node-desc " + ? "generic-node-desc " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5") (data.node?.description !== "" ? "py-5" : "pb-5")
: "" : ""
} }
> >
{data.node?.description !== "" && showNode && ( {data.node?.description !== "" &&
<div className="generic-node-desc-text"> showNode &&
data.node?.flow &&
inputDescription ? (
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}
}}
/>
) : (
<div
className="generic-node-desc-text"
onDoubleClick={() => setInputDescription(true)}
>
{data.node?.description} {data.node?.description}
</div> </div>
)} )}
<> <>
{Object.keys(data.node!.template) {Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_") .filter((templateField) => templateField.charAt(0) !== "_")
.sort()
.map((templateField: string, idx) => ( .map((templateField: string, idx) => (
<div key={idx}> <div key={idx}>
{data.node!.template[templateField].show && {data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? ( !data.node!.template[templateField].advanced ? (
<ParameterComponent <ParameterComponent
key={ index={idx.toString()}
(data.node!.template[templateField].input_types?.join( key={scapedJSONStringfy({
";" inputTypes:
) ?? data.node!.template[templateField].type) + data.node!.template[templateField].input_types,
"|" + type: data.node!.template[templateField].type,
templateField + id: data.id,
"|" + fieldName: templateField,
data.id proxy: data.node!.template[templateField].proxy,
} })}
data={data} data={data}
setData={setData} setData={setData}
color={ color={
@ -368,15 +443,10 @@ export default function GenericNode({
] ?? ] ??
nodeColors.unknown nodeColors.unknown
} }
title={ title={getFieldTitle(
data.node?.template[templateField].display_name data.node?.template!,
? data.node.template[templateField].display_name templateField
: data.node?.template[templateField].name )}
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
tooltipTitle={ tooltipTitle={
@ -384,21 +454,20 @@ export default function GenericNode({
"\n" "\n"
) ?? data.node?.template[templateField].type ) ?? data.node?.template[templateField].type
} }
required={data.node?.template[templateField].required} required={data.node!.template[templateField].required}
id={ id={{
(data.node?.template[templateField].input_types?.join( inputTypes:
";" data.node!.template[templateField].input_types,
) ?? data.node?.template[templateField].type) + type: data.node!.template[templateField].type,
"|" + id: data.id,
templateField + fieldName: templateField,
"|" + }}
data.id
}
left={true} left={true}
type={data.node?.template[templateField].type} type={data.node?.template[templateField].type}
optionalHandle={ optionalHandle={
data.node?.template[templateField].input_types data.node?.template[templateField].input_types
} }
proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
/> />
) : ( ) : (
@ -415,7 +484,11 @@ export default function GenericNode({
{" "} {" "}
</div> </div>
<ParameterComponent <ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")} key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data} data={data}
setData={setData} setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown} color={nodeColors[types[data.type]] ?? nodeColors.unknown}
@ -425,7 +498,11 @@ export default function GenericNode({
: data.type : data.type
} }
tooltipTitle={data.node?.base_classes.join("\n")} tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join("|")} id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode} showNode={showNode}

View file

@ -21,6 +21,7 @@ export default function DropdownButton({
<DropdownMenu open={showOptions}> <DropdownMenu open={showOptions}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
id="new-project-btn"
variant="primary" variant="primary"
className="relative pr-10" className="relative pr-10"
onClick={(event) => { onClick={(event) => {

View file

@ -72,7 +72,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
type="text" type="text"
name="name" name="name"
value={name ?? ""} value={name ?? ""}
placeholder="File name" placeholder="Flow name"
id="name" id="name"
maxLength={maxLength} maxLength={maxLength}
/> />

View file

@ -1,5 +1,5 @@
import { useContext } from "react"; import { useContext } from "react";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { cardComponentPropsType } from "../../types/components"; import { cardComponentPropsType } from "../../types/components";
import { gradients } from "../../utils/styleUtils"; import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
@ -17,7 +17,7 @@ export const CardComponent = ({
onDelete, onDelete,
button, button,
}: cardComponentPropsType): JSX.Element => { }: cardComponentPropsType): JSX.Element => {
const { removeFlow } = useContext(TabsContext); const { removeFlow } = useContext(FlowsContext);
return ( return (
<Card className="group"> <Card className="group">

View file

@ -7,7 +7,7 @@ import { typesContext } from "../../../contexts/typesContext";
import { postBuildInit } from "../../../controllers/API"; import { postBuildInit } from "../../../controllers/API";
import { FlowType } from "../../../types/flow"; import { FlowType } from "../../../types/flow";
import { TabsContext } from "../../../contexts/tabsContext"; import { FlowsContext } from "../../../contexts/flowsContext";
import { parsedDataType } from "../../../types/components"; import { parsedDataType } from "../../../types/components";
import { TabsState } from "../../../types/tabs"; import { TabsState } from "../../../types/tabs";
import { validateNodes } from "../../../utils/reactflowUtils"; import { validateNodes } from "../../../utils/reactflowUtils";
@ -26,7 +26,7 @@ export default function BuildTrigger({
}): JSX.Element { }): JSX.Element {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE(); const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(TabsContext); const { setTabsState } = useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false); const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : ""; const eventClick = isBuilding ? "pointer-events-none" : "";
@ -80,61 +80,54 @@ export default function BuildTrigger({
const { flowId } = response.data; const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource // Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = []; let validationResults: boolean[] = [];
let finished = false;
const apiUrl = `/api/v1/build/stream/${flowId}`; const apiUrl = `/api/v1/build/stream/${flowId}`;
const eventSource = new EventSource(apiUrl); return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
// If the event is parseable, return // If the event is parseable, return
if (!event.data) { if (!event.data) {
return; return;
} }
const parsedData = JSON.parse(event.data); const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection // if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) { if (parsedData.end_of_stream) {
// Close the connection and finish eventSource.close();
finished = true; resolve(validationResults.every((result) => result));
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
eventSource.close(); eventSource.close();
reject(new Error("Streaming failed"));
return; };
} else if (parsedData.log) { });
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
};
// Step 3: Wait for the stream to finish
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = validationResults.length === flow.data!.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
} }
function processStreamResult(parsedData: parsedDataType) { function processStreamResult(parsedData: parsedDataType) {

View file

@ -5,7 +5,7 @@ import BuildTrigger from "./buildTrigger";
import ChatTrigger from "./chatTrigger"; import ChatTrigger from "./chatTrigger";
import * as _ from "lodash"; import * as _ from "lodash";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API"; import { getBuildStatus } from "../../controllers/API";
import FormModal from "../../modals/formModal"; import FormModal from "../../modals/formModal";
import { NodeType } from "../../types/flow"; import { NodeType } from "../../types/flow";
@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType): JSX.Element { export default function Chat({ flow }: ChatType): JSX.Element {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false); const [canOpen, setCanOpen] = useState(false);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext); const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {

View file

@ -12,6 +12,8 @@ export default function CodeAreaComponent({
nodeClass, nodeClass,
dynamic, dynamic,
setNodeClass, setNodeClass,
id = "",
readonly = false,
}: CodeAreaComponentType) { }: CodeAreaComponentType) {
const [myValue, setMyValue] = useState( const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value) typeof value == "string" ? value : JSON.stringify(value)
@ -30,6 +32,7 @@ export default function CodeAreaComponent({
return ( return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}> <div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal <CodeAreaModal
readonly={readonly}
dynamic={dynamic} dynamic={dynamic}
value={myValue} value={myValue}
nodeClass={nodeClass} nodeClass={nodeClass}
@ -41,6 +44,7 @@ export default function CodeAreaComponent({
> >
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<span <span
id={id}
className={ className={
editNode editNode
? "input-edit-node input-dialog" ? "input-edit-node input-dialog"

View file

@ -63,7 +63,7 @@ export default function CodeTabsComponent({
}, [flow]); }, [flow]);
useEffect(() => { useEffect(() => {
if (tweaks) { if (tweaks && data) {
unselectAllNodes({ unselectAllNodes({
data, data,
updateNodes: (nodes) => { updateNodes: (nodes) => {
@ -604,6 +604,14 @@ export default function CodeTabsComponent({
].type === "prompt" ? ( ].type === "prompt" ? (
<div className="mx-auto"> <div className="mx-auto">
<PromptAreaComponent <PromptAreaComponent
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
editNode={true} editNode={true}
disabled={false} disabled={false}
value={ value={
@ -646,6 +654,14 @@ export default function CodeTabsComponent({
<CodeAreaComponent <CodeAreaComponent
disabled={false} disabled={false}
editNode={true} editNode={true}
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
value={ value={
!node.data.node.template[ !node.data.node.template[
templateField templateField

View file

@ -22,7 +22,6 @@ export default function DictComponent({
}, [value]); }, [value]);
const ref = useRef(value); const ref = useRef(value);
debugger;
return ( return (
<div <div
className={classNames( className={classNames(

View file

@ -23,6 +23,7 @@ export default function FloatComponent({
return ( return (
<div className="w-full"> <div className="w-full">
<Input <Input
id="float-input"
type="number" type="number"
step={step} step={step}
min={min} min={min}

View file

@ -1,5 +1,5 @@
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -17,7 +17,7 @@ import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button"; import { Button } from "../../../ui/button";
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => { export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
const { addFlow } = useContext(TabsContext); const { addFlow } = useContext(FlowsContext);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { undo, redo } = useContext(undoRedoContext); const { undo, redo } = useContext(undoRedoContext);
const [openSettings, setOpenSettings] = useState(false); const [openSettings, setOpenSettings] = useState(false);
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
function handleAddFlow() { function handleAddFlow() {
try { try {
addFlow(undefined, true).then((id) => { addFlow(true).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
// saveFlowStyleInDataBase(); // saveFlowStyleInDataBase();

View file

@ -6,7 +6,7 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { gradients } from "../../utils/styleUtils"; import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
@ -22,7 +22,7 @@ import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar"; import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element { export default function Header(): JSX.Element {
const { flows, tabId } = useContext(TabsContext); const { flows, tabId } = useContext(FlowsContext);
const { dark, setDark } = useContext(darkContext); const { dark, setDark } = useContext(darkContext);
const { notificationCenter } = useContext(alertContext); const { notificationCenter } = useContext(alertContext);
const location = useLocation(); const location = useLocation();

View file

@ -1,11 +1,13 @@
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { InputComponentType } from "../../types/components"; import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils"; import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
export default function InputComponent({ export default function InputComponent({
autoFocus = false,
onBlur,
value, value,
onChange, onChange,
disabled, disabled,
@ -15,9 +17,11 @@ export default function InputComponent({
editNode = false, editNode = false,
placeholder = "Type something...", placeholder = "Type something...",
className, className,
id = "",
blurOnEnter = false,
}: InputComponentType): JSX.Element { }: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -30,6 +34,10 @@ export default function InputComponent({
{isForm ? ( {isForm ? (
<Form.Control asChild> <Form.Control asChild>
<Input <Input
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"} type={password && !pwdVisible ? "password" : "text"}
value={value} value={value}
disabled={disabled} disabled={disabled}
@ -49,13 +57,18 @@ export default function InputComponent({
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
/> />
</Form.Control> </Form.Control>
) : ( ) : (
<Input <Input
id={id}
ref={refInput}
type="text" type="text"
onBlur={onBlur}
value={value} value={value}
autoFocus={autoFocus}
disabled={disabled} disabled={disabled}
required={required} required={required}
className={classNames( className={classNames(
@ -73,6 +86,7 @@ export default function InputComponent({
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
/> />
)} )}

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { uploadFile } from "../../controllers/API"; import { uploadFile } from "../../controllers/API";
import { FileComponentType } from "../../types/components"; import { FileComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
@ -17,7 +17,7 @@ export default function InputFileComponent({
const [myValue, setMyValue] = useState(value); const [myValue, setMyValue] = useState(value);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { tabId } = useContext(TabsContext); const { tabId } = useContext(FlowsContext);
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {

View file

@ -8,6 +8,7 @@ export default function IntComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
id = "",
}: FloatComponentType): JSX.Element { }: FloatComponentType): JSX.Element {
const min = 0; const min = 0;
@ -21,6 +22,7 @@ export default function IntComponent({
return ( return (
<div className="w-full"> <div className="w-full">
<Input <Input
id={id}
onKeyDown={(event) => { onKeyDown={(event) => {
if ( if (
event.key !== "Backspace" && event.key !== "Backspace" &&

View file

@ -55,6 +55,7 @@ export default function KeypairListComponent({
return ( return (
<div key={idx} className="flex w-full gap-2"> <div key={idx} className="flex w-full gap-2">
<Input <Input
id={"keypair" + index}
type="text" type="text"
value={key.trim()} value={key.trim()}
className={classNames( className={classNames(
@ -72,6 +73,7 @@ export default function KeypairListComponent({
/> />
<Input <Input
id={"keypair" + (index + 100).toString()}
type="text" type="text"
value={obj[key]} value={obj[key]}
className={editNode ? "input-edit-node" : ""} className={editNode ? "input-edit-node" : ""}
@ -88,6 +90,7 @@ export default function KeypairListComponent({
newInputList.push({ "": "" }); newInputList.push({ "": "" });
onChange(newInputList); onChange(newInputList);
}} }}
id={"plusbtn" + index.toString()}
> >
<IconComponent <IconComponent
name="Plus" name="Plus"
@ -101,6 +104,7 @@ export default function KeypairListComponent({
newInputList.splice(index, 1); newInputList.splice(index, 1);
onChange(newInputList); onChange(newInputList);
}} }}
id={"minusbtn" + index.toString()}
> >
<IconComponent <IconComponent
name="X" name="X"

View file

@ -14,7 +14,9 @@ export default function PromptAreaComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
}: PromptAreaComponentType) { id = "",
readonly = false,
}: PromptAreaComponentType): JSX.Element {
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
onChange(""); onChange("");
@ -22,7 +24,8 @@ export default function PromptAreaComponent({
}, [disabled]); }, [disabled]);
useEffect(() => { useEffect(() => {
if (value !== "" && !editNode) { //prevent update from prompt template after group node if prompt is wrongly marked as not dynamic
if (value !== "" && !editNode && !readonly && !nodeClass?.flow) {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => { postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) { if (apiReturn.data) {
setNodeClass!(apiReturn.data.frontend_node); setNodeClass!(apiReturn.data.frontend_node);
@ -35,6 +38,8 @@ export default function PromptAreaComponent({
return ( return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}> <div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal <GenericModal
id={id}
readonly={readonly}
type={TypeModal.PROMPT} type={TypeModal.PROMPT}
value={value} value={value}
buttonText="Check & Save" buttonText="Check & Save"
@ -47,6 +52,7 @@ export default function PromptAreaComponent({
> >
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<span <span
id={id}
className={ className={
editNode editNode
? "input-edit-node input-dialog" ? "input-edit-node input-dialog"

View file

@ -10,6 +10,7 @@ export default function TextAreaComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
id = "",
}: TextAreaComponentType): JSX.Element { }: TextAreaComponentType): JSX.Element {
// Clear text area // Clear text area
useEffect(() => { useEffect(() => {
@ -21,6 +22,7 @@ export default function TextAreaComponent({
return ( return (
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<Input <Input
id={id}
value={value} value={value}
disabled={disabled} disabled={disabled}
className={editNode ? "input-edit-node" : ""} className={editNode ? "input-edit-node" : ""}

View file

@ -6,6 +6,7 @@ export default function ToggleShadComponent({
setEnabled, setEnabled,
disabled, disabled,
size, size,
id = "",
}: ToggleComponentType): JSX.Element { }: ToggleComponentType): JSX.Element {
let scaleX, scaleY; let scaleX, scaleY;
switch (size) { switch (size) {
@ -29,6 +30,7 @@ export default function ToggleShadComponent({
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
<Switch <Switch
id={id}
style={{ style={{
transform: `scaleX(${scaleX}) scaleY(${scaleY})`, transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}} }}

View file

@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1", "z-50 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className className
)} )}
{...props} {...props}

View file

@ -1,5 +1,5 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import _ from "lodash"; import _, { cloneDeep } from "lodash";
import { import {
ReactNode, ReactNode,
createContext, createContext,
@ -21,10 +21,20 @@ import {
} from "../controllers/API"; } from "../controllers/API";
import { APIClassType, APITemplateType } from "../types/api"; import { APIClassType, APITemplateType } from "../types/api";
import { tweakType } from "../types/components"; import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow"; import {
import { TabsContextType, TabsState } from "../types/tabs"; FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { FlowsContextType, TabsState } from "../types/tabs";
import { import {
addVersionToDuplicates, addVersionToDuplicates,
checkOldEdgesHandles,
scapeJSONParse,
scapedJSONStringfy,
updateEdgesHandleIds,
updateIds, updateIds,
updateTemplate, updateTemplate,
} from "../utils/reactflowUtils"; } from "../utils/reactflowUtils";
@ -35,13 +45,13 @@ import { typesContext } from "./typesContext";
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
const TabsContextInitialValue: TabsContextType = { const FlowsContextInitialValue: FlowsContextType = {
tabId: "", tabId: "",
setTabId: (index: string) => {}, setTabId: (index: string) => {},
isLoading: true, isLoading: true,
flows: [], flows: [],
removeFlow: (id: string) => {}, removeFlow: (id: string) => {},
addFlow: async (flowData?: any) => "", addFlow: async (newProject: boolean, flowData?: FlowType) => "",
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(), incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
@ -65,8 +75,8 @@ const TabsContextInitialValue: TabsContextType = {
) => {}, ) => {},
}; };
export const TabsContext = createContext<TabsContextType>( export const FlowsContext = createContext<FlowsContextType>(
TabsContextInitialValue FlowsContextInitialValue
); );
export function TabsProvider({ children }: { children: ReactNode }) { export function TabsProvider({ children }: { children: ReactNode }) {
@ -134,14 +144,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (!flow.data) { if (!flow.data) {
return; return;
} }
processFlowEdges(flow); processDataFromFlow(flow, false);
processFlowNodes(flow);
} catch (e) {} } catch (e) {}
}); });
} }
function processFlowEdges(flow: FlowType) { function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return; if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}
//update edges colors
flow.data.edges.forEach((edge) => { flow.data.edges.forEach((edge) => {
edge.className = ""; edge.className = "";
edge.style = { stroke: "#555" }; edge.style = { stroke: "#555" };
@ -159,6 +173,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function processFlowNodes(flow: FlowType) { function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return; if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => { flow.data.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return; if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type]; const template = templates[node.data.type];
if (!template) { if (!template) {
@ -168,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (Object.keys(template["template"]).length > 0) { if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node, template); updateDisplay_name(node, template);
updateNodeBaseClasses(node, template); updateNodeBaseClasses(node, template);
//update baseclasses in edges
updateNodeEdges(flow, node, template); updateNodeEdges(flow, node, template);
updateNodeDescription(node, template); updateNodeDescription(node, template);
updateNodeTemplate(node, template); updateNodeTemplate(node, template);
@ -187,11 +203,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
) { ) {
flow.data!.edges.forEach((edge) => { flow.data!.edges.forEach((edge) => {
if (edge.source === node.id) { if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle let sourceHandleObject: sourceHandleType = scapeJSONParse(
?.split("|") edge.sourceHandle!
.slice(0, 2) );
.concat(template["base_classes"]) sourceHandleObject.baseClasses = template["base_classes"];
.join("|"); edge.data.sourceHandle = sourceHandleObject;
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
} }
}); });
} }
@ -243,9 +260,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download // simulate a click on the link element to trigger the download
link.click(); link.click();
setNoticeData({
title: "Warning: Critical data, JSON file may include API keys.",
});
} }
function downloadFlows() { function downloadFlows() {
@ -274,7 +288,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* The resulting JSON object is passed to the addFlow function. * The resulting JSON object is passed to the addFlow function.
*/ */
async function uploadFlow( async function uploadFlow(
newProject?: boolean, newProject: boolean,
file?: File file?: File
): Promise<String | undefined> { ): Promise<String | undefined> {
let id; let id;
@ -283,13 +297,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
let fileData = JSON.parse(text); let fileData = JSON.parse(text);
if (fileData.flows) { if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => { fileData.flows.forEach((flow: FlowType) => {
id = addFlow(flow, newProject); id = addFlow(newProject, flow);
}); });
} }
// parse the text into a JSON object // parse the text into a JSON object
let flow: FlowType = JSON.parse(text); let flow: FlowType = JSON.parse(text);
id = await addFlow(flow, newProject); id = await addFlow(newProject, flow);
} else { } else {
// create a file input // create a file input
const input = document.createElement("input"); const input = document.createElement("input");
@ -304,7 +318,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const currentfile = (e.target as HTMLInputElement).files![0]; const currentfile = (e.target as HTMLInputElement).files![0];
let text = await currentfile.text(); let text = await currentfile.text();
let flow: FlowType = JSON.parse(text); let flow: FlowType = JSON.parse(text);
const flowId = await addFlow(flow, newProject); const flowId = await addFlow(newProject, flow);
resolve(flowId); resolve(flowId);
} }
}; };
@ -355,7 +369,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Add a new flow to the list of flows. * Add a new flow to the list of flows.
* @param flow Optional flow to add. * @param flow Optional flow to add.
*/ */
function paste( function paste(
selectionInstance: { nodes: Node[]; edges: Edge[] }, selectionInstance: { nodes: Node[]; edges: Edge[] },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number }
@ -404,19 +417,28 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
reactFlowInstance!.setNodes(nodes); reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge) => { selectionInstance.edges.forEach((edge: Edge) => {
let source = idsMap[edge.source]; let source = idsMap[edge.source];
let target = idsMap[edge.target]; let target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|"); const sourceHandleObject: sourceHandleType = scapeJSONParse(
let sourceHandle = edge.sourceHandle!
sourceHandleSplitted[0] + );
"|" + let sourceHandle = scapedJSONStringfy({
source + ...sourceHandleObject,
"|" + id: source,
sourceHandleSplitted.slice(2).join("|"); });
let targetHandleSplitted = edge.targetHandle!.split("|"); sourceHandleObject.id = source;
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target; 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 = let id =
"reactflow__edge-" + "reactflow__edge-" +
source + source +
@ -431,12 +453,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
sourceHandle, sourceHandle,
targetHandle, targetHandle,
id, id,
data: cloneDeep(edge.data),
style: { stroke: "#555" }, style: { stroke: "#555" },
className: className:
targetHandle.split("|")[0] === "Text" targetHandleObject.type === "Text"
? "stroke-gray-800 " ? "stroke-gray-800 "
: "stroke-gray-900 ", : "stroke-gray-900 ",
animated: targetHandle.split("|")[0] === "Text", animated: targetHandleObject.type === "Text",
selected: false, selected: false,
}, },
edges.map((edge) => ({ ...edge, selected: false })) edges.map((edge) => ({ ...edge, selected: false }))
@ -446,19 +469,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
const addFlow = async ( const addFlow = async (
flow?: FlowType, newProject: Boolean,
newProject?: Boolean flow?: FlowType
): Promise<String | undefined> => { ): Promise<String | undefined> => {
if (newProject) { if (newProject) {
let flowData = extractDataFromFlow(flow!); let flowData = flow
if (flowData.description == "") { ? processDataFromFlow(flow)
flowData.description = getRandomDescription(); : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
}
// Create a new flow with a default name if no flow is provided. // Create a new flow with a default name if no flow is provided.
const newFlow = createNewFlow(flowData, flow!); const newFlow = createNewFlow(flowData, flow!);
processFlowEdges(newFlow);
processFlowNodes(newFlow);
const flowName = addVersionToDuplicates(newFlow, flows); const flowName = addVersionToDuplicates(newFlow, flows);
@ -486,31 +506,37 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
}; };
const extractDataFromFlow = (flow: FlowType) => { const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null; let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
if (data) { if (data) {
processFlowEdges(flow);
//prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges); updateEdges(data.edges);
updateNodes(data.nodes, data.edges); // updateNodes(data.nodes, data.edges);
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
} }
return { data, description }; return data;
}; };
const updateEdges = (edges: Edge[]) => { const updateEdges = (edges: Edge[]) => {
edges.forEach((edge) => { edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.className = edge.className =
(edge.targetHandle!.split("|")[0] === "Text" (targetHandleObject.type === "Text"
? "stroke-gray-800 " ? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection"; : "stroke-gray-900 ") + " stroke-connection";
edge.animated = edge.targetHandle!.split("|")[0] === "Text"; edge.animated = targetHandleObject.type === "Text";
}); });
}; };
const updateNodes = (nodes: Node[], edges: Edge[]) => { const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => { nodes.forEach((node) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return; if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type]; const template = templates[node.data.type];
if (!template) { if (!template) {
@ -520,12 +546,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (Object.keys(template["template"]).length > 0) { if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"]; node.data.node.base_classes = template["base_classes"];
edges.forEach((edge) => { edges.forEach((edge) => {
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
if (edge.source === node.id) { if (edge.source === node.id) {
edge.sourceHandle = edge let newSourceHandle = sourceHandleObject;
.sourceHandle!.split("|") newSourceHandle.baseClasses.concat(template["base_classes"]);
.slice(0, 2) edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
.concat(template["base_classes"])
.join("|");
} }
}); });
node.data.node.description = template["description"]; node.data.node.description = template["description"];
@ -538,12 +565,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}; };
const createNewFlow = ( const createNewFlow = (
flowData: { data: ReactFlowJsonObject | null; description: string }, flowData: ReactFlowJsonObject | null,
flow: FlowType flow: FlowType
) => ({ ) => ({
description: flowData.description, description: flow?.description ?? getRandomDescription(),
name: flow?.name ?? getRandomName(), name: flow?.name ?? getRandomName(),
data: flowData.data, data: flowData,
id: "", id: "",
}); });
@ -611,7 +638,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const [isBuilt, setIsBuilt] = useState(false); const [isBuilt, setIsBuilt] = useState(false);
return ( return (
<TabsContext.Provider <FlowsContext.Provider
value={{ value={{
saveFlow, saveFlow,
isBuilt, isBuilt,
@ -640,6 +667,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}} }}
> >
{children} {children}
</TabsContext.Provider> </FlowsContext.Provider>
); );
} }

View file

@ -7,8 +7,8 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext"; import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext"; import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext"; import { DarkProvider } from "./darkContext";
import { TabsProvider } from "./flowsContext";
import { LocationProvider } from "./locationContext"; import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext";
import { TypesProvider } from "./typesContext"; import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext"; import { UndoRedoProvider } from "./undoRedoContext";

View file

@ -13,7 +13,7 @@ import {
undoRedoContextType, undoRedoContextType,
} from "../types/typesContext"; } from "../types/typesContext";
import { isWrappedWithClass } from "../utils/utils"; import { isWrappedWithClass } from "../utils/utils";
import { TabsContext } from "./tabsContext"; import { FlowsContext } from "./flowsContext";
const initialValue = { const initialValue = {
undo: () => {}, undo: () => {},
@ -29,7 +29,7 @@ const defaultOptions: UseUndoRedoOptions = {
export const undoRedoContext = createContext<undoRedoContextType>(initialValue); export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
export function UndoRedoProvider({ children }) { export function UndoRedoProvider({ children }) {
const { tabId, flows } = useContext(TabsContext); const { tabId, flows } = useContext(FlowsContext);
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => [])); const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => [])); const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));

View file

@ -15,7 +15,7 @@ import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants"; import { EXPORT_CODE_DIALOG } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { TemplateVariableType } from "../../types/api"; import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components"; import { tweakType, uniqueTweakType } from "../../types/components";
import { FlowType, NodeType } from "../../types/flow/index"; import { FlowType, NodeType } from "../../types/flow/index";
@ -45,7 +45,7 @@ const ApiModal = forwardRef(
const [activeTab, setActiveTab] = useState("0"); const [activeTab, setActiveTab] = useState("0");
const tweak = useRef<tweakType>([]); const tweak = useRef<tweakType>([]);
const tweaksList = useRef<string[]>([]); const tweaksList = useRef<string[]>([]);
const { setTweak, getTweak, tabsState } = useContext(TabsContext); const { setTweak, getTweak, tabsState } = useContext(FlowsContext);
const pythonApiCode = getPythonApiCode( const pythonApiCode = getPythonApiCode(
flow, flow,
autoLogin, autoLogin,

View file

@ -7,6 +7,8 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent"; import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent"; import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent"; import Dropdown from "../../components/dropdownComponent";
@ -31,7 +33,7 @@ import {
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { limitScrollFieldsModal } from "../../constants/constants"; import { limitScrollFieldsModal } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs"; import { TabsState } from "../../types/tabs";
@ -63,10 +65,11 @@ const EditNodeModal = forwardRef(
ref ref
) => { ) => {
const [modalOpen, setModalOpen] = useState(open ?? false); const [modalOpen, setModalOpen] = useState(open ?? false);
const updateNodeInternals = useUpdateNodeInternals();
const myData = useRef(data); const myData = useRef(data);
const { setTabsState, tabId } = useContext(TabsContext); const { setTabsState, tabId } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
let disabled = let disabled =
reactFlowInstance reactFlowInstance
@ -82,11 +85,14 @@ const EditNodeModal = forwardRef(
const handleOnNewValue = (newValue: any, name) => { const handleOnNewValue = (newValue: any, name) => {
myData.current.node!.template[name].value = newValue; myData.current.node!.template[name].value = newValue;
setDataValue(newValue); setDataValue(newValue);
updateNodeInternals(data.id);
}; };
useEffect(() => { useEffect(() => {
myData.current = data; // reset data to what it is on node when opening modal if (modalOpen) {
onClose!(modalOpen); myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
}
}, [modalOpen]); }, [modalOpen]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
@ -166,11 +172,27 @@ const EditNodeModal = forwardRef(
.map((templateParam, index) => ( .map((templateParam, index) => (
<TableRow key={index} className="h-10"> <TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3"> <TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
{myData.current.node?.template[templateParam].name <ShadTooltip
? myData.current.node.template[templateParam] content={
.name myData.current.node?.template[templateParam]
: myData.current.node?.template[templateParam] .proxy
.display_name} ? myData.current.node?.template[
templateParam
].proxy?.id
: null
}
>
<span>
{myData.current.node?.template[templateParam]
.display_name
? myData.current.node.template[
templateParam
].display_name
: myData.current.node?.template[
templateParam
].name}
</span>
</ShadTooltip>
</TableCell> </TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground "> <TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.current.node?.template[templateParam] {myData.current.node?.template[templateParam]
@ -203,6 +225,7 @@ const EditNodeModal = forwardRef(
templateParam templateParam
].multiline ? ( ].multiline ? (
<TextAreaComponent <TextAreaComponent
id={"textarea-edit-" + index}
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
@ -216,6 +239,7 @@ const EditNodeModal = forwardRef(
/> />
) : ( ) : (
<InputComponent <InputComponent
id={"input-" + index}
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
password={ password={
@ -311,6 +335,7 @@ const EditNodeModal = forwardRef(
<div className="ml-auto"> <div className="ml-auto">
{" "} {" "}
<ToggleShadComponent <ToggleShadComponent
id={"toggle-edit-" + index}
disabled={disabled} disabled={disabled}
enabled={ enabled={
myData.current.node.template[ myData.current.node.template[
@ -369,6 +394,7 @@ const EditNodeModal = forwardRef(
.type === "int" ? ( .type === "int" ? (
<div className="mx-auto"> <div className="mx-auto">
<IntComponent <IntComponent
id={"int-input-" + index}
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
@ -416,6 +442,9 @@ const EditNodeModal = forwardRef(
.type === "prompt" ? ( .type === "prompt" ? (
<div className="mx-auto"> <div className="mx-auto">
<PromptAreaComponent <PromptAreaComponent
readonly={
myData.current.node?.flow ? true : false
}
field_name={templateParam} field_name={templateParam}
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
@ -431,12 +460,21 @@ const EditNodeModal = forwardRef(
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
id={"prompt-area-edit" + index}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.current.node?.template[templateParam]
.type === "code" ? ( .type === "code" ? (
<div className="mx-auto"> <div className="mx-auto">
<CodeAreaComponent <CodeAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
}
dynamic={ dynamic={
data.node!.template[templateParam] data.node!.template[templateParam]
.dynamic ?? false .dynamic ?? false
@ -455,6 +493,7 @@ const EditNodeModal = forwardRef(
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
id={"code-area-edit" + index}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.current.node?.template[templateParam]
@ -467,6 +506,11 @@ const EditNodeModal = forwardRef(
<TableCell className="p-0 text-right"> <TableCell className="p-0 text-right">
<div className="items-center text-center"> <div className="items-center text-center">
<ToggleShadComponent <ToggleShadComponent
id={
"show" +
myData.current.node?.template[templateParam]
.name
}
enabled={ enabled={
!myData.current.node?.template[ !myData.current.node?.template[
templateParam templateParam
@ -492,6 +536,7 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer> <BaseModal.Footer>
<Button <Button
id={"saveChangesBtn"}
className="mt-3" className="mt-3"
onClick={() => { onClick={() => {
const newData = cloneDeep(myData.current); const newData = cloneDeep(myData.current);

View file

@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants"; import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
@ -23,6 +24,7 @@ export default function CodeAreaModal({
setNodeClass, setNodeClass,
children, children,
dynamic, dynamic,
readonly = false,
}: codeAreaModalPropsType): JSX.Element { }: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value); const [code, setCode] = useState(value);
const { dark } = useContext(darkContext); const { dark } = useContext(darkContext);
@ -143,9 +145,15 @@ export default function CodeAreaModal({
/> />
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
<Input
value={code}
className="absolute left-[500%] top-[500%]"
id="codeValue"
/>
<div className="flex h-full w-full flex-col transition-all"> <div className="flex h-full w-full flex-col transition-all">
<div className="h-full w-full"> <div className="h-full w-full">
<AceEditor <AceEditor
readOnly={readonly}
value={code} value={code}
mode="python" mode="python"
height={height ?? "100%"} height={height ?? "100%"}
@ -180,7 +188,13 @@ export default function CodeAreaModal({
</div> </div>
</div> </div>
<div className="flex h-fit w-full justify-end"> <div className="flex h-fit w-full justify-end">
<Button className="mt-3" onClick={handleClick} type="submit"> <Button
className="mt-3"
onClick={handleClick}
type="submit"
id="checkAndSaveBtn"
disabled={readonly}
>
Check & Save Check & Save
</Button> </Button>
</div> </div>

View file

@ -4,14 +4,18 @@ import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants"; import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext"; import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { removeApiKeys } from "../../utils/reactflowUtils"; import { removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
const ExportModal = forwardRef( const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => { (props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, downloadFlow } = useContext(TabsContext); const { flows, tabId, downloadFlow } = useContext(FlowsContext);
const [checked, setChecked] = useState(false); const { reactFlowInstance } = useContext(typesContext);
const { setNoticeData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
const flow = flows.find((f) => f.id === tabId); const flow = flows.find((f) => f.id === tabId);
useEffect(() => { useEffect(() => {
setName(flow!.name); setName(flow!.name);
@ -44,6 +48,7 @@ const ExportModal = forwardRef(
<div className="mt-3 flex items-center space-x-2"> <div className="mt-3 flex items-center space-x-2">
<Checkbox <Checkbox
id="terms" id="terms"
checked={checked}
onCheckedChange={(event: boolean) => { onCheckedChange={(event: boolean) => {
setChecked(event); setChecked(event);
}} }}
@ -52,20 +57,38 @@ const ExportModal = forwardRef(
Save with my API keys Save with my API keys
</label> </label>
</div> </div>
<span className="text-xs text-destructive">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer> <BaseModal.Footer>
<Button <Button
onClick={() => { onClick={() => {
if (checked) if (checked) {
downloadFlow( downloadFlow(
flows.find((flow) => flow.id === tabId)!, {
id: tabId,
data: reactFlowInstance?.toObject()!,
description,
name,
},
name!, name!,
description description
); );
else setNoticeData({
title:
"Warning: Critical data, JSON file may include API keys.",
});
} else
downloadFlow( downloadFlow(
removeApiKeys(flows.find((flow) => flow.id === tabId)!), removeApiKeys({
id: tabId,
data: reactFlowInstance?.toObject()!,
description,
name,
}),
name!, name!,
description description
); );

View file

@ -3,7 +3,7 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants"; import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { FlowSettingsPropsType } from "../../types/components"; import { FlowSettingsPropsType } from "../../types/components";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
@ -11,7 +11,7 @@ export default function FlowSettingsModal({
open, open,
setOpen, setOpen,
}: FlowSettingsPropsType): JSX.Element { }: FlowSettingsPropsType): JSX.Element {
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext); const { flows, tabId, updateFlow, saveFlow } = useContext(FlowsContext);
const flow = flows.find((f) => f.id === tabId); const flow = flows.find((f) => f.id === tabId);
useEffect(() => { useEffect(() => {
setName(flow!.name); setName(flow!.name);

View file

@ -24,7 +24,7 @@ import {
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants"; import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API"; import { getBuildStatus } from "../../controllers/API";
import { TabsState } from "../../types/tabs"; import { TabsState } from "../../types/tabs";
import { validateNodes } from "../../utils/reactflowUtils"; import { validateNodes } from "../../utils/reactflowUtils";
@ -38,7 +38,7 @@ export default function FormModal({
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
flow: FlowType; flow: FlowType;
}): JSX.Element { }): JSX.Element {
const { tabsState, setTabsState } = useContext(TabsContext); const { tabsState, setTabsState } = useContext(FlowsContext);
const [chatValue, setChatValue] = useState(() => { const [chatValue, setChatValue] = useState(() => {
try { try {
const { formKeysData } = tabsState[flow.id]; const { formKeysData } = tabsState[flow.id];

View file

@ -30,6 +30,8 @@ export default function GenericModal({
nodeClass, nodeClass,
setNodeClass, setNodeClass,
children, children,
id = "",
readonly = false,
}: genericModalPropsType): JSX.Element { }: genericModalPropsType): JSX.Element {
const [myButtonText] = useState(buttonText); const [myButtonText] = useState(buttonText);
const [myModalTitle] = useState(modalTitle); const [myModalTitle] = useState(modalTitle);
@ -208,8 +210,9 @@ export default function GenericModal({
"flex h-full w-full" "flex h-full w-full"
)} )}
> >
{type === TypeModal.PROMPT && isEdit ? ( {type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea <Textarea
id={"modal-" + id}
ref={divRefPrompt} ref={divRefPrompt}
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1" className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
value={inputValue} value={inputValue}
@ -226,7 +229,7 @@ export default function GenericModal({
handleKeyDown(e, inputValue, ""); handleKeyDown(e, inputValue, "");
}} }}
/> />
) : type === TypeModal.PROMPT && !isEdit ? ( ) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
<SanitizedHTMLWrapper <SanitizedHTMLWrapper
className={getClassByNumberLength()} className={getClassByNumberLength()}
content={coloredContent} content={coloredContent}
@ -248,6 +251,7 @@ export default function GenericModal({
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
}} }}
readOnly={readonly}
/> />
) : ( ) : (
<></> <></>
@ -284,7 +288,7 @@ export default function GenericModal({
className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm" className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm"
> >
<div className="relative bottom-[1px]"> <div className="relative bottom-[1px]">
<span> <span id={"badge" + index.toString()}>
{word.replace(/[{}]/g, "").length > 59 {word.replace(/[{}]/g, "").length > 59
? word.replace(/[{}]/g, "").slice(0, 56) + ? word.replace(/[{}]/g, "").slice(0, 56) +
"..." "..."
@ -304,6 +308,8 @@ export default function GenericModal({
)} )}
</div> </div>
<Button <Button
id="genericModalBtnSave"
disabled={readonly}
onClick={() => { onClick={() => {
switch (myModalType) { switch (myModalType) {
case TypeModal.TEXT: case TypeModal.TEXT:

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -22,7 +22,7 @@ import {
} from "../../constants/constants"; } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { import {
addUser, addUser,
deleteUser, deleteUser,
@ -44,7 +44,7 @@ export default function AdminPage() {
const { userData } = useContext(AuthContext); const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0); const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext); const { setTabId } = useContext(FlowsContext);
// set null id // set null id
useEffect(() => { useEffect(() => {

View file

@ -1,7 +1,7 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { CardComponent } from "../../components/cardComponent"; import { CardComponent } from "../../components/cardComponent";
@ -12,7 +12,7 @@ import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow"; import { FlowType } from "../../types/flow";
export default function CommunityPage(): JSX.Element { export default function CommunityPage(): JSX.Element {
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } = const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
useContext(TabsContext); useContext(FlowsContext);
// set null id // set null id
useEffect(() => { useEffect(() => {
@ -94,7 +94,7 @@ export default function CommunityPage(): JSX.Element {
size="sm" size="sm"
className="whitespace-nowrap " className="whitespace-nowrap "
onClick={() => { onClick={() => {
addFlow(flow, true).then((id) => { addFlow(true, flow).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
}} }}

View file

@ -14,6 +14,7 @@ export default function DisclosureComponent({
<div> <div>
<Disclosure.Button className="components-disclosure-arrangement"> <Disclosure.Button className="components-disclosure-arrangement">
<div className="flex gap-4"> <div className="flex gap-4">
{/* BUG ON THIS ICON */}
<Icon strokeWidth={1.5} size={22} className="text-primary" /> <Icon strokeWidth={1.5} size={22} className="text-primary" />
<span className="components-disclosure-title">{title}</span> <span className="components-disclosure-title">{title}</span>
</div> </div>

View file

@ -28,16 +28,23 @@ import ReactFlow, {
import GenericNode from "../../../../CustomNodes/GenericNode"; import GenericNode from "../../../../CustomNodes/GenericNode";
import Chat from "../../../../components/chatComponent"; import Chat from "../../../../components/chatComponent";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { locationContext } from "../../../../contexts/locationContext"; import { locationContext } from "../../../../contexts/locationContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { APIClassType } from "../../../../types/api"; import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow"; import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
import { TabsState } from "../../../../types/tabs"; import { TabsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils"; import {
import { isWrappedWithClass } from "../../../../utils/utils"; generateFlow,
generateNodeFromFlow,
isValidConnection,
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent"; import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent"; import ExtraSidebar from "../extraSidebarComponent";
const nodeTypes = { const nodeTypes = {
@ -63,7 +70,7 @@ export default function Page({
saveFlow, saveFlow,
setTabsState, setTabsState,
tabId, tabId,
} = useContext(TabsContext); } = useContext(FlowsContext);
const { const {
types, types,
reactFlowInstance, reactFlowInstance,
@ -238,12 +245,19 @@ export default function Page({
addEdge( addEdge(
{ {
...params, ...params,
data: {
targetHandle: scapeJSONParse(params.targetHandle!),
sourceHandle: scapeJSONParse(params.sourceHandle!),
},
style: { stroke: "#555" }, style: { stroke: "#555" },
className: className:
(params.targetHandle?.split("|")[0] === "Text" ((scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text"
? "stroke-foreground " ? "stroke-foreground "
: "stroke-foreground ") + " stroke-connection", : "stroke-foreground ") + " stroke-connection",
animated: params.targetHandle?.split("|")[0] === "Text", animated:
(scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text",
}, },
eds eds
) )
@ -296,7 +310,6 @@ export default function Page({
event.dataTransfer.getData("nodedata") event.dataTransfer.getData("nodedata")
); );
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
// Calculate the position where the node should be created // Calculate the position where the node should be created
const position = reactFlowInstance!.project({ const position = reactFlowInstance!.project({
x: event.clientX - reactflowBounds!.left, x: event.clientX - reactflowBounds!.left,
@ -394,7 +407,7 @@ export default function Page({
edgeUpdateSuccessful.current = true; edgeUpdateSuccessful.current = true;
}, []); }, []);
const [selectionEnded, setSelectionEnded] = useState(false); const [selectionEnded, setSelectionEnded] = useState(true);
const onSelectionEnd = useCallback(() => { const onSelectionEnd = useCallback(() => {
setSelectionEnded(true); setSelectionEnded(true);
@ -434,7 +447,7 @@ export default function Page({
<div className="h-full w-full" ref={reactFlowWrapper}> <div className="h-full w-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 && {Object.keys(templates).length > 0 &&
Object.keys(types).length > 0 ? ( Object.keys(types).length > 0 ? (
<div className="h-full w-full"> <div id="react-flow-id" className="h-full w-full">
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
onMove={() => { onMove={() => {
@ -481,6 +494,50 @@ export default function Page({
[&>button]:border-b-border hover:[&>button]:bg-border" [&>button]:border-b-border hover:[&>button]:bg-border"
></Controls> ></Controls>
)} )}
<SelectionMenu
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
lastSelection!,
reactFlowInstance!,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(
newFlow,
getNodeId
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
!lastSelection?.nodes.some(
(selectionNode) =>
selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
} else {
setErrorData({
title: "Invalid selection",
list: validateSelection(lastSelection!, edges),
});
}
}}
/>
</ReactFlow> </ReactFlow>
{!view && ( {!view && (
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} /> <Chat flow={flow} reactFlowInstance={reactFlowInstance!} />

View file

@ -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>
);
}

View file

@ -5,7 +5,7 @@ import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input"; import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator"; import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import ApiModal from "../../../../modals/ApiModal"; import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal"; import ExportModal from "../../../../modals/exportModal";
@ -22,7 +22,7 @@ export default function ExtraSidebar(): JSX.Element {
const { data, templates, getFilterEdge, setFilterEdge } = const { data, templates, getFilterEdge, setFilterEdge } =
useContext(typesContext); useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } = const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
useContext(TabsContext); useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext); const { setSuccessData, setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data); const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@ -136,7 +136,7 @@ export default function ExtraSidebar(): JSX.Element {
<button <button
className="extra-side-bar-buttons" className="extra-side-bar-buttons"
onClick={() => { onClick={() => {
uploadFlow(); uploadFlow(false);
}} }}
> >
<IconComponent name="FileUp" className="side-bar-button-size " /> <IconComponent name="FileUp" className="side-bar-button-size " />
@ -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"> <span className="side-bar-components-text">
{data[SBSectionName][SBItemName].display_name} {data[SBSectionName][SBItemName].display_name}
</span> </span>

View file

@ -8,15 +8,20 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
} from "../../../../components/ui/select-custom"; } from "../../../../components/ui/select-custom";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import EditNodeModal from "../../../../modals/EditNodeModal"; import EditNodeModal from "../../../../modals/EditNodeModal";
import { nodeToolbarPropsType } from "../../../../types/components"; import { nodeToolbarPropsType } from "../../../../types/components";
import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils"; import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
export default function NodeToolbarComponent({ export default function NodeToolbarComponent({
data, data,
setData, setData,
deleteNode, deleteNode,
position,
setShowNode, setShowNode,
numberOfHandles, numberOfHandles,
showNode, showNode,
@ -47,7 +52,9 @@ export default function NodeToolbarComponent({
return true; return true;
} }
const isMinimal = canMinimize(); const isMinimal = canMinimize();
const { paste } = useContext(TabsContext); const isGroup = data.node?.flow ? true : false;
const { paste } = useContext(FlowsContext);
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false); const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [selectedValue, setSelectedValue] = useState(""); const [selectedValue, setSelectedValue] = useState("");
@ -65,6 +72,10 @@ export default function NodeToolbarComponent({
if (event.includes("disabled")) { if (event.includes("disabled")) {
return; return;
} }
if (event.includes("ungroup")) {
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance);
}
}; };
return ( return (
@ -134,11 +145,11 @@ export default function NodeToolbarComponent({
</a> </a>
</ShadTooltip> </ShadTooltip>
{isMinimal ? ( {isMinimal || isGroup ? (
<Select onValueChange={handleSelectChange} value={selectedValue}> <Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top"> <ShadTooltip content="More" side="top">
<SelectTrigger> <SelectTrigger>
<div> <div id="advancedIcon">
<div <div
className={classNames( 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" + "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 <div
id="editAdvancedBtn"
className={ className={
"flex " + "flex " +
(nodeLength == 0 (nodeLength == 0
@ -179,7 +191,7 @@ export default function NodeToolbarComponent({
</SelectItem> </SelectItem>
{isMinimal && ( {isMinimal && (
<SelectItem value={getRandomKeyByssmm() + "show"}> <SelectItem value={getRandomKeyByssmm() + "show"}>
<div className="flex"> <div className="flex" id="editAdvanced">
<IconComponent <IconComponent
name={showNode ? "Minimize2" : "Maximize2"} name={showNode ? "Minimize2" : "Maximize2"}
className="relative top-0.5 mr-2 h-4 w-4" className="relative top-0.5 mr-2 h-4 w-4"
@ -188,11 +200,22 @@ export default function NodeToolbarComponent({
</div> </div>
</SelectItem> </SelectItem>
)} )}
{isGroup && (
<SelectItem value={getRandomKeyByssmm() + "ungroup"}>
<div className="flex">
<IconComponent
name="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent> </SelectContent>
</Select> </Select>
) : ( ) : (
<ShadTooltip content="Edit" side="top"> <ShadTooltip content="Edit" side="top">
<div> <div id="editAdvancedIcon">
<button <button
disabled={nodeLength === 0} disabled={nodeLength === 0}
onClick={() => setShowModalAdvanced(true)} onClick={() => setShowModalAdvanced(true)}
@ -222,29 +245,6 @@ export default function NodeToolbarComponent({
<></> <></>
</EditNodeModal> </EditNodeModal>
)} )}
{/*
<ShadTooltip content="Edit" side="top">
<div>
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
>
<div
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(!canMinimize() && " rounded-r-md ") +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</div>
</EditNodeModal>
</div>
</ShadTooltip> */}
</span> </span>
</div> </div>
</> </>

View file

@ -1,12 +1,12 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { getVersion } from "../../controllers/API"; import { getVersion } from "../../controllers/API";
import Page from "./components/PageComponent"; import Page from "./components/PageComponent";
export default function FlowPage(): JSX.Element { export default function FlowPage(): JSX.Element {
const { flows, tabId, setTabId } = useContext(TabsContext); const { flows, tabId, setTabId } = useContext(FlowsContext);
const { id } = useParams(); const { id } = useParams();
// Set flow tab id // Set flow tab id

View file

@ -8,7 +8,7 @@ import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants"; import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
export default function HomePage(): JSX.Element { export default function HomePage(): JSX.Element {
const { const {
flows, flows,
@ -19,7 +19,7 @@ export default function HomePage(): JSX.Element {
removeFlow, removeFlow,
uploadFlow, uploadFlow,
isLoading, isLoading,
} = useContext(TabsContext); } = useContext(FlowsContext);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const dropdownOptions = [ const dropdownOptions = [
{ {
@ -104,7 +104,7 @@ export default function HomePage(): JSX.Element {
<DropdownButton <DropdownButton
firstButtonName="New Project" firstButtonName="New Project"
onFirstBtnClick={() => { onFirstBtnClick={() => {
addFlow(null!, true).then((id) => { addFlow(true).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
}} }}

View file

@ -9,7 +9,7 @@ import { Button } from "../../components/ui/button";
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants"; import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { resetPassword, updateUser } from "../../controllers/API"; import { resetPassword, updateUser } from "../../controllers/API";
import { import {
inputHandlerEventType, inputHandlerEventType,
@ -17,7 +17,7 @@ import {
} from "../../types/components"; } from "../../types/components";
import { gradients } from "../../utils/styleUtils"; import { gradients } from "../../utils/styleUtils";
export default function ProfileSettingsPage(): JSX.Element { export default function ProfileSettingsPage(): JSX.Element {
const { setTabId } = useContext(TabsContext); const { setTabId } = useContext(FlowsContext);
const [inputState, setInputState] = useState<patchUserInputStateType>( const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE CONTROL_PATCH_USER_STATE

View file

@ -1,28 +1,18 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { darkContext } from "../../contexts/darkContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getVersion } from "../../controllers/API"; import { getVersion } from "../../controllers/API";
import Page from "../FlowPage/components/PageComponent"; import Page from "../FlowPage/components/PageComponent";
export default function ViewPage() { export default function ViewPage() {
const { flows, tabId, setTabId } = useContext(TabsContext); const { flows, tabId, setTabId } = useContext(FlowsContext);
const { setDark } = useContext(darkContext); const { id } = useParams();
const { id, theme } = useParams();
// Set flow tab id // Set flow tab id
useEffect(() => { useEffect(() => {
setTabId(id!); setTabId(id!);
}, [id]); }, [id]);
useEffect(() => {
if (theme) {
setDark(theme === "dark");
} else {
setDark(false);
}
}, [theme]);
// Initialize state variable for the version // Initialize state variable for the version
const [version, setVersion] = useState(""); const [version, setVersion] = useState("");
useEffect(() => { useEffect(() => {

View file

@ -44,7 +44,7 @@ const Router = () => {
} }
/> />
<Route <Route
path="view/:theme?" path="view"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<ViewPage /> <ViewPage />

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