Generic Loaders (#964)
This commit is contained in:
commit
07cfca4949
128 changed files with 10995 additions and 2067 deletions
|
|
@ -15,7 +15,7 @@
|
|||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "make install_frontend && make install_backend",
|
||||
"postCreateCommand": "make setup_devcontainer",
|
||||
|
||||
"containerEnv": {
|
||||
"POETRY_VIRTUALENVS_IN_PROJECT": "true"
|
||||
|
|
@ -31,11 +31,13 @@
|
|||
"sourcery.sourcery",
|
||||
"eamodio.gitlens",
|
||||
"ms-vscode.makefile-tools",
|
||||
"GitHub.vscode-pull-request-github"
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"Codium.codium",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,14 @@ LANGFLOW_REMOVE_API_KEYS=
|
|||
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
|
||||
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
|
||||
# Example: LANGFLOW_SUPERUSER=admin
|
||||
LANGFLOW_SUPERUSER=
|
||||
|
|
|
|||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
|
@ -4,10 +4,18 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
|
|
|
|||
9
.github/workflows/lint.yml
vendored
9
.github/workflows/lint.yml
vendored
|
|
@ -3,7 +3,16 @@ name: lint
|
|||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.4.0"
|
||||
|
|
|
|||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
|
@ -3,9 +3,17 @@ name: test
|
|||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
|
||||
paths:
|
||||
- "tests/**"
|
||||
- "src/backend/**"
|
||||
- "pyproject.toml"
|
||||
env:
|
||||
POETRY_VERSION: "1.5.0"
|
||||
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -254,4 +254,6 @@ langflow.db
|
|||
|
||||
/tmp/*
|
||||
src/backend/langflow/frontend/
|
||||
.docker
|
||||
.docker
|
||||
|
||||
.idea
|
||||
27
Makefile
27
Makefile
|
|
@ -22,13 +22,19 @@ tests:
|
|||
@make install_backend
|
||||
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:
|
||||
poetry run black .
|
||||
poetry run ruff . --fix
|
||||
cd src/frontend && npm run format
|
||||
|
||||
lint:
|
||||
make install_backend
|
||||
poetry run mypy src/backend/langflow
|
||||
poetry run black . --check
|
||||
poetry run ruff . --fix
|
||||
|
|
@ -43,19 +49,20 @@ run_frontend:
|
|||
cd src/frontend && npm start
|
||||
|
||||
run_cli:
|
||||
poetry run langflow run --path src/frontend/build
|
||||
poetry run langflow --path src/frontend/build
|
||||
|
||||
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:
|
||||
make init
|
||||
make build_frontend
|
||||
poetry run langflow --path src/frontend/build
|
||||
@echo 'Run Cli'
|
||||
make run_cli
|
||||
|
||||
frontend:
|
||||
make install_frontend
|
||||
make run_frontend
|
||||
@-make install_frontend || (echo "An error occurred while installing frontend dependencies. Attempting to fix." && make install_frontendc)
|
||||
@make run_frontend
|
||||
|
||||
frontendc:
|
||||
make install_frontendc
|
||||
|
|
@ -66,7 +73,13 @@ install_backend:
|
|||
|
||||
backend:
|
||||
make install_backend
|
||||
poetry run uvicorn --factory src.backend.langflow.main:create_app --port 7860 --reload --log-level debug
|
||||
ifeq ($(login),1)
|
||||
@echo "Running backend without autologin";
|
||||
poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser
|
||||
else
|
||||
@echo "Running backend with autologin";
|
||||
LANGFLOW_AUTO_LOGIN=True poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser
|
||||
endif
|
||||
|
||||
build_and_run:
|
||||
echo 'Removing dist folder'
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
|
|||
|
||||
# 🎨 Creating Flows
|
||||
|
||||
Creating flows with Langflow is easy. Simply drag sidebar components onto the canvas and connect them together to create your pipeline. Langflow provides a range of [LangChain components](https://langchain.readthedocs.io/en/latest/reference.html) to choose from, including LLMs, prompt serializers, agents, and chains.
|
||||
Creating flows with Langflow is easy. Simply drag sidebar components onto the canvas and connect them together to create your pipeline. Langflow provides a range of [LangChain components](https://docs.langchain.com/docs/category/components) to choose from, including LLMs, prompt serializers, agents, and chains.
|
||||
|
||||
Explore by editing prompt parameters, link chains and agents, track an agent's thought process, and export your flow.
|
||||
|
||||
|
|
|
|||
|
|
@ -217,4 +217,40 @@ Vertex AI is a cloud computing platform offered by Google Cloud Platform (GCP).
|
|||
- **top_k:** How the model selects tokens for output, the next token is selected from – defaults to `40`.
|
||||
- **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.
|
||||
- **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.
|
||||
BIN
docs/static/videos/langflow_fork.mp4
vendored
BIN
docs/static/videos/langflow_fork.mp4
vendored
Binary file not shown.
BIN
docs/static/videos/langflow_parameters.mp4
vendored
BIN
docs/static/videos/langflow_parameters.mp4
vendored
Binary file not shown.
BIN
docs/static/videos/langflow_widget.mp4
vendored
BIN
docs/static/videos/langflow_widget.mp4
vendored
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2 MiB After Width: | Height: | Size: 2 MiB |
1281
poetry.lock
generated
1281
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -33,7 +33,7 @@ google-search-results = "^2.4.1"
|
|||
google-api-python-client = "^2.79.0"
|
||||
typer = "^0.9.0"
|
||||
gunicorn = "^21.2.0"
|
||||
langchain = "^0.0.320"
|
||||
langchain = "^0.0.312"
|
||||
openai = "^0.27.8"
|
||||
pandas = "2.0.3"
|
||||
chromadb = "^0.3.21"
|
||||
|
|
@ -90,7 +90,11 @@ langfuse = "^1.0.13"
|
|||
pillow = "^10.0.0"
|
||||
metal-sdk = "^2.2.0"
|
||||
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]
|
||||
types-redis = "^4.6.0.5"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ def set_var_for_macos_issue():
|
|||
import os
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -187,14 +187,18 @@ async def stream_build(
|
|||
valid = False
|
||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||
|
||||
response = {
|
||||
"valid": valid,
|
||||
"params": params,
|
||||
"id": vertex.id,
|
||||
"progress": round(i / number_of_nodes, 2),
|
||||
}
|
||||
vertex_id = (
|
||||
vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
|
||||
)
|
||||
if vertex_id in graph.top_level_nodes:
|
||||
response = {
|
||||
"valid": valid,
|
||||
"params": params,
|
||||
"id": vertex_id,
|
||||
"progress": round(i / number_of_nodes, 2),
|
||||
}
|
||||
|
||||
yield str(StreamData(event="message", data=response))
|
||||
yield str(StreamData(event="message", data=response))
|
||||
|
||||
langchain_object = graph.build()
|
||||
# Now we need to check the input_keys to send them to the client
|
||||
|
|
|
|||
|
|
@ -54,10 +54,9 @@ def get_all(
|
|||
|
||||
logger.debug("Building langchain types dict")
|
||||
try:
|
||||
types_dict = get_all_types_dict(settings_service)
|
||||
return get_all_types_dict(settings_service)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return types_dict
|
||||
|
||||
|
||||
# For backwards compatibility we will keep the old endpoint
|
||||
|
|
@ -70,7 +69,7 @@ def get_all(
|
|||
"/process/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
)
|
||||
async def process_flow(
|
||||
async def process(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[dict] = None,
|
||||
|
|
|
|||
|
|
@ -79,4 +79,5 @@ class ConversationalAgent(CustomComponent):
|
|||
memory=memory,
|
||||
verbose=True,
|
||||
return_intermediate_steps=True,
|
||||
handle_parsing_errors=True,
|
||||
)
|
||||
|
|
|
|||
232
src/backend/langflow/components/documentloaders/FileLoader.py
Normal file
232
src/backend/langflow/components/documentloaders/FileLoader.py
Normal 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()
|
||||
62
src/backend/langflow/components/documentloaders/UrlLoader.py
Normal file
62
src/backend/langflow/components/documentloaders/UrlLoader.py
Normal 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()
|
||||
45
src/backend/langflow/components/llms/AmazonBedrock.py
Normal file
45
src/backend/langflow/components/llms/AmazonBedrock.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.llms import HuggingFaceEndpoint
|
||||
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
from langchain.llms.base import BaseLLM
|
||||
|
||||
|
||||
|
|
@ -13,7 +13,6 @@ class HuggingFaceEndpointsComponent(CustomComponent):
|
|||
"endpoint_url": {"display_name": "Endpoint URL", "password": True},
|
||||
"task": {
|
||||
"display_name": "Task",
|
||||
"type": "select",
|
||||
"options": ["text2text-generation", "text-generation", "summarization"],
|
||||
},
|
||||
"huggingfacehub_api_token": {"display_name": "API token", "password": True},
|
||||
|
|
@ -27,7 +26,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
|
|||
def build(
|
||||
self,
|
||||
endpoint_url: str,
|
||||
task="text2text-generation",
|
||||
task: str = "text2text-generation",
|
||||
huggingfacehub_api_token: Optional[str] = None,
|
||||
model_kwargs: Optional[dict] = None,
|
||||
) -> BaseLLM:
|
||||
|
|
@ -36,6 +35,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
|
|||
endpoint_url=endpoint_url,
|
||||
task=task,
|
||||
huggingfacehub_api_token=huggingfacehub_api_token,
|
||||
model_kwargs=model_kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
|
||||
|
|
|
|||
48
src/backend/langflow/components/retrievers/AmazonKendra.py
Normal file
48
src/backend/langflow/components/retrievers/AmazonKendra.py
Normal 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
|
||||
|
|
@ -14,7 +14,7 @@ class ChromaComponent(CustomComponent):
|
|||
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"
|
||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/chroma"
|
||||
beta = True
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from langchain.vectorstores import Vectara
|
|||
from langchain.schema import Document
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain.embeddings.base import Embeddings
|
||||
|
||||
|
||||
class VectaraComponent(CustomComponent):
|
||||
|
|
@ -22,7 +21,6 @@ class VectaraComponent(CustomComponent):
|
|||
"vectara_api_key": {"display_name": "Vectara API Key", "password": True},
|
||||
"code": {"show": False},
|
||||
"documents": {"display_name": "Documents"},
|
||||
"embedding": {"display_name": "Embedding"},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -30,21 +28,21 @@ class VectaraComponent(CustomComponent):
|
|||
vectara_customer_id: str,
|
||||
vectara_corpus_id: str,
|
||||
vectara_api_key: str,
|
||||
embedding: Optional[Embeddings] = None,
|
||||
documents: Optional[Document] = None,
|
||||
) -> Union[VectorStore, BaseRetriever]:
|
||||
# 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(
|
||||
documents=documents, # type: ignore
|
||||
vectara_customer_id=vectara_customer_id,
|
||||
vectara_corpus_id=vectara_corpus_id,
|
||||
vectara_api_key=vectara_api_key,
|
||||
embedding=embedding,
|
||||
source="langflow",
|
||||
)
|
||||
|
||||
return Vectara(
|
||||
vectara_customer_id=vectara_customer_id,
|
||||
vectara_corpus_id=vectara_corpus_id,
|
||||
vectara_api_key=vectara_api_key,
|
||||
source="langflow",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -265,8 +265,8 @@ retrievers:
|
|||
# ZepRetriever:
|
||||
# documentation: "https://python.langchain.com/docs/modules/data_connection/retrievers/integrations/zep_memorystore"
|
||||
vectorstores:
|
||||
Chroma:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/chroma"
|
||||
# Chroma:
|
||||
# documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/chroma"
|
||||
Qdrant:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/qdrant"
|
||||
Weaviate:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
from typing import Union, Dict
|
||||
|
||||
# Type alias for more complex dicts
|
||||
NestedDict = Dict[str, Union[str, Dict]]
|
||||
50
src/backend/langflow/field_typing/constants.py
Normal file
50
src/backend/langflow/field_typing/constants.py
Normal 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,
|
||||
}
|
||||
|
|
@ -1,28 +1,79 @@
|
|||
from loguru import logger
|
||||
from typing import TYPE_CHECKING
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
|
||||
|
||||
class SourceHandle(BaseModel):
|
||||
baseClasses: List[str] = Field(
|
||||
..., description="List of base classes for the source handle."
|
||||
)
|
||||
dataType: str = Field(..., description="Data type for the source handle.")
|
||||
id: str = Field(..., description="Unique identifier for the source handle.")
|
||||
|
||||
|
||||
class TargetHandle(BaseModel):
|
||||
fieldName: str = Field(..., description="Field name for the target handle.")
|
||||
id: str = Field(..., description="Unique identifier for the target handle.")
|
||||
inputTypes: Optional[List[str]] = Field(
|
||||
None, description="List of input types for the target handle."
|
||||
)
|
||||
type: str = Field(..., description="Type of the target handle.")
|
||||
|
||||
|
||||
class Edge:
|
||||
def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
|
||||
self.source: "Vertex" = source
|
||||
self.target: "Vertex" = target
|
||||
self.source_handle = edge.get("sourceHandle", "")
|
||||
self.target_handle = edge.get("targetHandle", "")
|
||||
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
|
||||
# target_param is documents
|
||||
self.target_param = self.target_handle.split("|")[1]
|
||||
|
||||
if data := edge.get("data", {}):
|
||||
self._source_handle = data.get("sourceHandle", {})
|
||||
self._target_handle = data.get("targetHandle", {})
|
||||
self.source_handle: SourceHandle = SourceHandle(**self._source_handle)
|
||||
self.target_handle: TargetHandle = TargetHandle(**self._target_handle)
|
||||
self.target_param = self.target_handle.fieldName
|
||||
# validate handles
|
||||
self.validate_handles()
|
||||
else:
|
||||
# Logging here because this is a breaking change
|
||||
logger.error("Edge data is empty")
|
||||
self._source_handle = edge.get("sourceHandle", "")
|
||||
self._target_handle = edge.get("targetHandle", "")
|
||||
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
|
||||
# target_param is documents
|
||||
self.target_param = self._target_handle.split("|")[1]
|
||||
# Validate in __init__ to fail fast
|
||||
self.validate_edge()
|
||||
|
||||
def validate_handles(self) -> None:
|
||||
if self.target_handle.inputTypes is None:
|
||||
self.valid_handles = (
|
||||
self.target_handle.type in self.source_handle.baseClasses
|
||||
)
|
||||
else:
|
||||
self.valid_handles = (
|
||||
any(
|
||||
baseClass in self.target_handle.inputTypes
|
||||
for baseClass in self.source_handle.baseClasses
|
||||
)
|
||||
or self.target_handle.type in self.source_handle.baseClasses
|
||||
)
|
||||
if not self.valid_handles:
|
||||
logger.debug(self.source_handle)
|
||||
logger.debug(self.target_handle)
|
||||
raise ValueError(
|
||||
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
|
||||
f"has invalid handles"
|
||||
)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.source = state["source"]
|
||||
self.target = state["target"]
|
||||
self.target_param = state["target_param"]
|
||||
self.source_handle = state["source_handle"]
|
||||
self.target_handle = state["target_handle"]
|
||||
self.source_handle = state.get("source_handle")
|
||||
self.target_handle = state.get("target_handle")
|
||||
|
||||
def reset(self) -> None:
|
||||
self.source._build_params()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from typing import Dict, Generator, List, Type, Union
|
|||
|
||||
from langflow.graph.edge.base import Edge
|
||||
from langflow.graph.graph.constants import lazy_load_vertex_dict
|
||||
from langflow.graph.graph.utils import process_flow
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.graph.vertex.types import (
|
||||
FileToolVertex,
|
||||
|
|
@ -19,11 +20,21 @@ class Graph:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]],
|
||||
nodes: List[Dict],
|
||||
edges: List[Dict[str, str]],
|
||||
) -> None:
|
||||
self._nodes = nodes
|
||||
self._edges = edges
|
||||
self.raw_graph_data = {"nodes": nodes, "edges": edges}
|
||||
|
||||
self.top_level_nodes = []
|
||||
for node in self._nodes:
|
||||
if node_id := node.get("id"):
|
||||
self.top_level_nodes.append(node_id)
|
||||
|
||||
self._graph_data = process_flow(self.raw_graph_data)
|
||||
self._nodes = self._graph_data["nodes"]
|
||||
self._edges = self._graph_data["edges"]
|
||||
self._build_graph()
|
||||
|
||||
def __setstate__(self, state):
|
||||
|
|
@ -50,6 +61,7 @@ class Graph:
|
|||
edges = payload["edges"]
|
||||
return cls(nodes, edges)
|
||||
except KeyError as exc:
|
||||
logger.exception(exc)
|
||||
raise ValueError(
|
||||
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
|
||||
) from exc
|
||||
|
|
@ -215,7 +227,9 @@ class Graph:
|
|||
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
|
||||
|
||||
VertexClass = self._get_vertex_class(node_type, node_lc_type)
|
||||
nodes.append(VertexClass(node))
|
||||
vertex = VertexClass(node)
|
||||
vertex.set_top_level(self.top_level_nodes)
|
||||
nodes.append(vertex)
|
||||
|
||||
return nodes
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
from collections import deque
|
||||
import copy
|
||||
|
||||
|
||||
def find_last_node(nodes, edges):
|
||||
"""
|
||||
This function receives a flow and returns the last node.
|
||||
"""
|
||||
return next((n for n in nodes if all(e["source"] != n["id"] for e in edges)), None)
|
||||
|
||||
|
||||
def add_parent_node_id(nodes, parent_node_id):
|
||||
"""
|
||||
This function receives a list of nodes and adds a parent_node_id to each node.
|
||||
"""
|
||||
for node in nodes:
|
||||
node["parent_node_id"] = parent_node_id
|
||||
|
||||
|
||||
def ungroup_node(group_node_data, base_flow):
|
||||
template, flow = (
|
||||
group_node_data["node"]["template"],
|
||||
group_node_data["node"]["flow"],
|
||||
)
|
||||
parent_node_id = group_node_data["id"]
|
||||
g_nodes = flow["data"]["nodes"]
|
||||
add_parent_node_id(g_nodes, parent_node_id)
|
||||
g_edges = flow["data"]["edges"]
|
||||
|
||||
# Redirect edges to the correct proxy node
|
||||
updated_edges = get_updated_edges(
|
||||
base_flow, g_nodes, g_edges, group_node_data["id"]
|
||||
)
|
||||
|
||||
# Update template values
|
||||
update_template(template, g_nodes)
|
||||
|
||||
nodes = [
|
||||
n for n in base_flow["nodes"] if n["id"] != group_node_data["id"]
|
||||
] + g_nodes
|
||||
edges = (
|
||||
[
|
||||
e
|
||||
for e in base_flow["edges"]
|
||||
if e["target"] != group_node_data["id"]
|
||||
and e["source"] != group_node_data["id"]
|
||||
]
|
||||
+ g_edges
|
||||
+ updated_edges
|
||||
)
|
||||
|
||||
base_flow["nodes"] = nodes
|
||||
base_flow["edges"] = edges
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def process_flow(flow_object):
|
||||
cloned_flow = copy.deepcopy(flow_object)
|
||||
processed_nodes = set() # To keep track of processed nodes
|
||||
|
||||
def process_node(node):
|
||||
node_id = node.get("id")
|
||||
|
||||
# If node already processed, skip
|
||||
if node_id in processed_nodes:
|
||||
return
|
||||
|
||||
if (
|
||||
node.get("data")
|
||||
and node["data"].get("node")
|
||||
and node["data"]["node"].get("flow")
|
||||
):
|
||||
process_flow(node["data"]["node"]["flow"]["data"])
|
||||
new_nodes = ungroup_node(node["data"], cloned_flow)
|
||||
# Add new nodes to the queue for future processing
|
||||
nodes_to_process.extend(new_nodes)
|
||||
|
||||
# Mark node as processed
|
||||
processed_nodes.add(node_id)
|
||||
|
||||
nodes_to_process = deque(cloned_flow["nodes"])
|
||||
|
||||
while nodes_to_process:
|
||||
node = nodes_to_process.popleft()
|
||||
process_node(node)
|
||||
|
||||
return cloned_flow
|
||||
|
||||
|
||||
def update_template(template, g_nodes):
|
||||
"""
|
||||
Updates the template of a node in a graph with the given template.
|
||||
|
||||
Args:
|
||||
template (dict): The new template to update the node with.
|
||||
g_nodes (list): The list of nodes in the graph.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
for _, value in template.items():
|
||||
if not value.get("proxy"):
|
||||
continue
|
||||
proxy_dict = value["proxy"]
|
||||
field, id_ = proxy_dict["field"], proxy_dict["id"]
|
||||
node_index = next((i for i, n in enumerate(g_nodes) if n["id"] == id_), -1)
|
||||
if node_index != -1:
|
||||
display_name = None
|
||||
show = g_nodes[node_index]["data"]["node"]["template"][field]["show"]
|
||||
advanced = g_nodes[node_index]["data"]["node"]["template"][field][
|
||||
"advanced"
|
||||
]
|
||||
if "display_name" in g_nodes[node_index]["data"]["node"]["template"][field]:
|
||||
display_name = g_nodes[node_index]["data"]["node"]["template"][field][
|
||||
"display_name"
|
||||
]
|
||||
else:
|
||||
display_name = g_nodes[node_index]["data"]["node"]["template"][field][
|
||||
"name"
|
||||
]
|
||||
|
||||
g_nodes[node_index]["data"]["node"]["template"][field] = value
|
||||
g_nodes[node_index]["data"]["node"]["template"][field]["show"] = show
|
||||
g_nodes[node_index]["data"]["node"]["template"][field][
|
||||
"advanced"
|
||||
] = advanced
|
||||
g_nodes[node_index]["data"]["node"]["template"][field][
|
||||
"display_name"
|
||||
] = display_name
|
||||
|
||||
|
||||
def update_target_handle(new_edge, g_nodes, group_node_id):
|
||||
"""
|
||||
Updates the target handle of a given edge if it is a proxy node.
|
||||
|
||||
Args:
|
||||
new_edge (dict): The edge to update.
|
||||
g_nodes (list): The list of nodes in the graph.
|
||||
group_node_id (str): The ID of the group node.
|
||||
|
||||
Returns:
|
||||
dict: The updated edge.
|
||||
"""
|
||||
target_handle = new_edge["data"]["targetHandle"]
|
||||
if target_handle.get("proxy"):
|
||||
proxy_id = target_handle["proxy"]["id"]
|
||||
if node := next((n for n in g_nodes if n["id"] == proxy_id), None):
|
||||
set_new_target_handle(proxy_id, new_edge, target_handle, node)
|
||||
return new_edge
|
||||
|
||||
|
||||
def set_new_target_handle(proxy_id, new_edge, target_handle, node):
|
||||
"""
|
||||
Sets a new target handle for a given edge.
|
||||
|
||||
Args:
|
||||
proxy_id (str): The ID of the proxy.
|
||||
new_edge (dict): The new edge to be created.
|
||||
target_handle (dict): The target handle of the edge.
|
||||
node (dict): The node containing the edge.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
new_edge["target"] = proxy_id
|
||||
_type = target_handle.get("type")
|
||||
if _type is None:
|
||||
raise KeyError("The 'type' key must be present in target_handle.")
|
||||
|
||||
field = target_handle["proxy"]["field"]
|
||||
new_target_handle = {
|
||||
"fieldName": field,
|
||||
"type": _type,
|
||||
"id": proxy_id,
|
||||
}
|
||||
if node["data"]["node"].get("flow"):
|
||||
new_target_handle["proxy"] = {
|
||||
"field": node["data"]["node"]["template"][field]["proxy"]["field"],
|
||||
"id": node["data"]["node"]["template"][field]["proxy"]["id"],
|
||||
}
|
||||
if input_types := target_handle.get("inputTypes"):
|
||||
new_target_handle["inputTypes"] = input_types
|
||||
new_edge["data"]["targetHandle"] = new_target_handle
|
||||
|
||||
|
||||
def update_source_handle(new_edge, g_nodes, g_edges):
|
||||
"""
|
||||
Updates the source handle of a given edge to the last node in the flow data.
|
||||
|
||||
Args:
|
||||
new_edge (dict): The edge to update.
|
||||
flow_data (dict): The flow data containing the nodes and edges.
|
||||
|
||||
Returns:
|
||||
dict: The updated edge with the new source handle.
|
||||
"""
|
||||
last_node = copy.deepcopy(find_last_node(g_nodes, g_edges))
|
||||
new_edge["source"] = last_node["id"]
|
||||
new_source_handle = new_edge["data"]["sourceHandle"]
|
||||
new_source_handle["id"] = last_node["id"]
|
||||
new_edge["data"]["sourceHandle"] = new_source_handle
|
||||
return new_edge
|
||||
|
||||
|
||||
def get_updated_edges(base_flow, g_nodes, g_edges, group_node_id):
|
||||
"""
|
||||
Given a base flow, a list of graph nodes and a group node id, returns a list of updated edges.
|
||||
An updated edge is an edge that has its target or source handle updated based on the group node id.
|
||||
|
||||
Args:
|
||||
base_flow (dict): The base flow containing a list of edges.
|
||||
g_nodes (list): A list of graph nodes.
|
||||
group_node_id (str): The id of the group node.
|
||||
|
||||
Returns:
|
||||
list: A list of updated edges.
|
||||
"""
|
||||
updated_edges = []
|
||||
for edge in base_flow["edges"]:
|
||||
new_edge = copy.deepcopy(edge)
|
||||
if new_edge["target"] == group_node_id:
|
||||
new_edge = update_target_handle(new_edge, g_nodes, group_node_id)
|
||||
|
||||
if new_edge["source"] == group_node_id:
|
||||
new_edge = update_source_handle(new_edge, g_nodes, g_edges)
|
||||
|
||||
if edge["target"] == group_node_id or edge["source"] == group_node_id:
|
||||
updated_edges.append(new_edge)
|
||||
return updated_edges
|
||||
|
|
@ -38,6 +38,8 @@ class Vertex:
|
|||
self.task_id: Optional[str] = None
|
||||
self.is_task = is_task
|
||||
self.params = params or {}
|
||||
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
|
||||
self.parent_is_top_level = False
|
||||
|
||||
def reset_params(self):
|
||||
for edge in self.edges:
|
||||
|
|
@ -88,6 +90,11 @@ class Vertex:
|
|||
self._built = False
|
||||
self.artifacts: Dict[str, Any] = {}
|
||||
self.task_id: Optional[str] = None
|
||||
self.parent_node_id = state["parent_node_id"]
|
||||
self.parent_is_top_level = state["parent_is_top_level"]
|
||||
|
||||
def set_top_level(self, top_level_nodes: List[str]) -> None:
|
||||
self.parent_is_top_level = self.parent_node_id in top_level_nodes
|
||||
|
||||
def _parse_data(self) -> None:
|
||||
self.data = self._data["data"]
|
||||
|
|
@ -209,6 +216,16 @@ class Vertex:
|
|||
}
|
||||
elif isinstance(_value, dict):
|
||||
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:
|
||||
params[key] = value.get("value")
|
||||
|
||||
|
|
@ -342,7 +359,7 @@ class Vertex:
|
|||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
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
|
||||
|
||||
def _update_built_object_and_artifacts(self, result):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.chains import LLMChain
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.schema import Document
|
||||
from langflow.field_typing import (
|
||||
Tool,
|
||||
PromptTemplate,
|
||||
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"
|
||||
description: str = "Create any custom component you want!"
|
||||
|
||||
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))
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any, Callable, List, Optional, Union
|
||||
from uuid import UUID
|
||||
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.directory_reader import DirectoryReader
|
||||
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
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from langchain.agents.load_tools import (
|
|||
_EXTRA_OPTIONAL_TOOLS,
|
||||
_LLM_TOOLS,
|
||||
)
|
||||
from langchain.tools.python.tool import PythonInputs
|
||||
|
||||
from langflow.custom import customs
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
|
|
@ -162,14 +161,8 @@ class ToolCreator(LangChainTypeCreator):
|
|||
template = Template(fields=fields, type_name=tool_type)
|
||||
|
||||
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 {
|
||||
"template": util.format_dict(template_dict),
|
||||
"template": util.format_dict(template.to_dict()),
|
||||
**tool_params,
|
||||
"base_classes": base_classes,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import Any, List
|
|||
from langflow.api.utils import get_new_key
|
||||
from langflow.interface.agents.base import agent_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.document_loaders.base import documentloader_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)
|
||||
|
||||
|
||||
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):
|
||||
"""Build a custom component template for the langchain"""
|
||||
try:
|
||||
|
|
@ -314,6 +332,9 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
|
|||
add_base_classes(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
add_output_types(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
logger.debug("Added base classes")
|
||||
return frontend_node
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -28,14 +28,10 @@ class UtilityCreator(LangChainTypeCreator):
|
|||
"""
|
||||
if self.type_dict is None:
|
||||
settings_service = get_settings_service()
|
||||
self.type_dict = {}
|
||||
for utility_name in utilities.__all__:
|
||||
try:
|
||||
imported = import_class(f"langchain.utilities.{utility_name}")
|
||||
self.type_dict[utility_name] = imported
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.type_dict = {
|
||||
utility_name: import_class(f"langchain.utilities.{utility_name}")
|
||||
for utility_name in utilities.__all__
|
||||
}
|
||||
self.type_dict["SQLDatabase"] = utilities.SQLDatabase
|
||||
# Filter according to settings.utilities
|
||||
self.type_dict = {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ def get_langfuse_callback(trace_id):
|
|||
if langfuse := LangfuseInstance.get():
|
||||
logger.debug("Langfuse credentials found")
|
||||
try:
|
||||
trace = langfuse.trace(CreateTrace(id=trace_id))
|
||||
trace = langfuse.trace(
|
||||
CreateTrace(name="langflow-" + trace_id, id=trace_id)
|
||||
)
|
||||
return trace.getNewHandler()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error initializing langfuse callback: {exc}")
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ def initialize_database():
|
|||
try:
|
||||
database_service.run_migrations()
|
||||
except CommandError as exc:
|
||||
if "Can't locate revision identified by" not in str(exc):
|
||||
# if "overlaps with other requested revisions" or "Can't locate revision identified by"
|
||||
# are not in the exception, we can't handle it
|
||||
if "overlaps with other requested revisions" not in str(
|
||||
exc
|
||||
) and "Can't locate revision identified by" not in str(exc):
|
||||
raise exc
|
||||
# This means there's wrong revision in the DB
|
||||
# We need to delete the alembic_version table
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class FieldFormatters(BaseModel):
|
|||
|
||||
class FrontendNode(BaseModel):
|
||||
template: Template
|
||||
description: str
|
||||
description: Optional[str] = None
|
||||
base_classes: List[str]
|
||||
name: str = ""
|
||||
display_name: str = ""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from langflow.template.field.base import TemplateField
|
|||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.template.base import Template
|
||||
from langflow.interface.custom.constants import DEFAULT_CUSTOM_COMPONENT_CODE
|
||||
from typing import Optional
|
||||
|
||||
|
||||
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] = []
|
||||
|
||||
def to_dict(self):
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class PromptFrontendNode(FrontendNode):
|
|||
|
||||
# All prompt fields should be password=False
|
||||
field.password = False
|
||||
field.dynamic = True
|
||||
|
||||
|
||||
class PromptTemplateNode(FrontendNode):
|
||||
|
|
|
|||
|
|
@ -191,7 +191,9 @@ def get_base_classes(cls):
|
|||
"""Get the base classes of a class.
|
||||
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 = []
|
||||
for base in bases:
|
||||
if any(type in base.__module__ for type in ["pydantic", "abc"]):
|
||||
|
|
|
|||
2
src/frontend/.gitignore
vendored
2
src/frontend/.gitignore
vendored
|
|
@ -22,5 +22,5 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright-report/*/
|
||||
/playwright/.cache/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
1726
src/frontend/package-lock.json
generated
1726
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
src/frontend/playwright-report/index.html
Normal file
18
src/frontend/playwright-report/index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="./e2e/index.html">e2e report</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./onlyFront/index.html">frontEnd Only report</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -20,11 +20,13 @@ export default defineConfig({
|
|||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
reporter: [
|
||||
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
// baseURL: "http://127.0.0.1:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
|
@ -69,9 +71,16 @@ export default defineConfig({
|
|||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
// webServer: [
|
||||
// {
|
||||
// command: "npm run backend",
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// timeout: 120 * 1000,
|
||||
// },
|
||||
// {
|
||||
// command: "npm run start",
|
||||
// url: "http://127.0.0.1:3000",
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
// ],
|
||||
});
|
||||
|
|
|
|||
78
src/frontend/run-tests.sh
Executable file
78
src/frontend/run-tests.sh
Executable 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
|
||||
|
|
@ -16,8 +16,8 @@ import {
|
|||
FETCH_ERROR_MESSAGE,
|
||||
} from "./constants/constants";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { FlowsContext } from "./contexts/flowsContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import { TabsContext } from "./contexts/tabsContext";
|
||||
import { typesContext } from "./contexts/typesContext";
|
||||
import Router from "./routes";
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export default function App() {
|
|||
setShowSideBar(true);
|
||||
setIsStackedOpen(true);
|
||||
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
|
||||
const { hardReset } = useContext(TabsContext);
|
||||
const { hardReset } = useContext(FlowsContext);
|
||||
|
||||
const {
|
||||
errorData,
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ import TextAreaComponent from "../../../../components/textAreaComponent";
|
|||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { ParameterComponentType } from "../../../../types/components";
|
||||
import { TabsState } from "../../../../types/tabs";
|
||||
import {
|
||||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
isValidConnection,
|
||||
scapedJSONStringfy,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import {
|
||||
nodeColors,
|
||||
|
|
@ -53,14 +53,16 @@ export default function ParameterComponent({
|
|||
required = false,
|
||||
optionalHandle = null,
|
||||
info = "",
|
||||
proxy,
|
||||
showNode,
|
||||
index = "",
|
||||
}: ParameterComponentType): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
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;
|
||||
|
||||
|
|
@ -80,8 +82,9 @@ export default function ParameterComponent({
|
|||
|
||||
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
|
||||
false;
|
||||
reactFlowInstance
|
||||
?.getEdges()
|
||||
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
|
||||
|
||||
const { data: myData } = useContext(typesContext);
|
||||
|
||||
|
|
@ -112,7 +115,6 @@ export default function ParameterComponent({
|
|||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (name === "openai_api_base") console.log(info);
|
||||
// @ts-ignore
|
||||
infoHtml.current = (
|
||||
<div className="h-full w-full break-words">
|
||||
|
|
@ -210,32 +212,40 @@ export default function ParameterComponent({
|
|||
!optionalHandle ? (
|
||||
<></>
|
||||
) : (
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||
<div className="flex">
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
|
|
@ -250,7 +260,13 @@ export default function ParameterComponent({
|
|||
(info !== "" ? " flex items-center" : "")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{proxy ? (
|
||||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||
<span>{title}</span>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
<div className="">
|
||||
{info !== "" && (
|
||||
|
|
@ -290,7 +306,11 @@ export default function ParameterComponent({
|
|||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
|
|
@ -331,9 +351,11 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"textarea-" + index}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={"input-" + index}
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
|
|
@ -344,6 +366,7 @@ export default function ParameterComponent({
|
|||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<ToggleShadComponent
|
||||
id={"toggle-" + index}
|
||||
disabled={disabled}
|
||||
enabled={data.node?.template[name].value ?? false}
|
||||
setEnabled={(isEnabled) => {
|
||||
|
|
@ -373,6 +396,11 @@ export default function ParameterComponent({
|
|||
) : left === true && type === "code" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
data.node?.flow && data.node.template[name].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={data.node?.template[name].dynamic ?? false}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
|
|
@ -381,6 +409,7 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"code-input-" + index}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "file" ? (
|
||||
|
|
@ -402,11 +431,13 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"int-input-" + index}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<PromptAreaComponent
|
||||
readonly={data.node?.flow ? true : false}
|
||||
field_name={name}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
|
|
@ -420,6 +451,7 @@ export default function ParameterComponent({
|
|||
onChange={(e) => {
|
||||
handleOnNewValue(e);
|
||||
}}
|
||||
id={"prompt-input-" + index}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "NestedDict" ? (
|
||||
|
|
|
|||
|
|
@ -4,30 +4,46 @@ import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
|||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { useSSE } from "../../contexts/SSEContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { cleanEdges } from "../../utils/reactflowUtils";
|
||||
import {
|
||||
cleanEdges,
|
||||
handleKeyDown,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, toTitleCase } from "../../utils/utils";
|
||||
import { classNames, getFieldTitle } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
|
||||
export default function GenericNode({
|
||||
data: olddata,
|
||||
xPos,
|
||||
yPos,
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}): JSX.Element {
|
||||
const [data, setData] = useState(olddata);
|
||||
const { updateFlow, flows, tabId } = useContext(TabsContext);
|
||||
const { updateFlow, flows, tabId } = useContext(FlowsContext);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
|
||||
useContext(typesContext);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
const [inputName, setInputName] = useState(true);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!
|
||||
);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<validationStatusType | null>(null);
|
||||
const [showNode, setShowNode] = useState<boolean>(true);
|
||||
|
|
@ -111,6 +127,7 @@ export default function GenericNode({
|
|||
<>
|
||||
<NodeToolbar>
|
||||
<NodeToolbarComponent
|
||||
position={{ x: xPos, y: yPos }}
|
||||
data={data}
|
||||
setData={setData}
|
||||
deleteNode={deleteNode}
|
||||
|
|
@ -151,7 +168,7 @@ export default function GenericNode({
|
|||
}
|
||||
>
|
||||
<IconComponent
|
||||
name={name}
|
||||
name={data.node?.flow ? "Ungroup" : name}
|
||||
className={
|
||||
"generic-node-icon " +
|
||||
(!showNode && "absolute inset-x-6 h-12 w-12")
|
||||
|
|
@ -160,11 +177,35 @@ export default function GenericNode({
|
|||
/>
|
||||
{showNode && (
|
||||
<div className="generic-node-tooltip-div">
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div className="generic-node-tooltip-div text-primary">
|
||||
{data.node?.display_name}
|
||||
{data.node?.flow && inputName ? (
|
||||
<div>
|
||||
<InputComponent
|
||||
autoFocus
|
||||
onBlur={() => {
|
||||
setInputName(false);
|
||||
if (nodeName.trim() !== "") {
|
||||
setNodeName(nodeName);
|
||||
data.node!.display_name = nodeName;
|
||||
} else {
|
||||
setNodeName(data.node!.display_name);
|
||||
}
|
||||
}}
|
||||
value={nodeName}
|
||||
onChange={setNodeName}
|
||||
password={false}
|
||||
blurOnEnter={true}
|
||||
/>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
onDoubleClick={() => setInputName(true)}
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -178,16 +219,15 @@ export default function GenericNode({
|
|||
data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced && (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node!.template[
|
||||
templateField
|
||||
].input_types?.join(";") ??
|
||||
data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
index={idx.toString()}
|
||||
key={scapedJSONStringfy({
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
|
|
@ -199,15 +239,10 @@ export default function GenericNode({
|
|||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node?.template[templateField].display_name
|
||||
? data.node.template[templateField].display_name
|
||||
: data.node?.template[templateField].name
|
||||
? toTitleCase(
|
||||
data.node.template[templateField].name
|
||||
)
|
||||
: toTitleCase(templateField)
|
||||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
tooltipTitle={
|
||||
|
|
@ -217,31 +252,31 @@ export default function GenericNode({
|
|||
data.node?.template[templateField].type
|
||||
}
|
||||
required={
|
||||
data.node?.template[templateField].required
|
||||
}
|
||||
id={
|
||||
(data.node?.template[
|
||||
templateField
|
||||
].input_types?.join(";") ??
|
||||
data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
data.node!.template[templateField].required
|
||||
}
|
||||
id={{
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
}}
|
||||
left={true}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<ParameterComponent
|
||||
key={[data.type, data.id, ...data.node!.base_classes].join(
|
||||
"|"
|
||||
)}
|
||||
key={scapedJSONStringfy({
|
||||
baseClasses: data.node!.base_classes,
|
||||
id: data.id,
|
||||
dataType: data.type,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
|
|
@ -252,9 +287,11 @@ export default function GenericNode({
|
|||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
id={[data.type, data.id, ...data.node!.base_classes].join(
|
||||
"|"
|
||||
)}
|
||||
id={{
|
||||
baseClasses: data.node!.base_classes,
|
||||
id: data.id,
|
||||
dataType: data.type,
|
||||
}}
|
||||
type={data.node?.base_classes.join("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
|
|
@ -330,33 +367,71 @@ export default function GenericNode({
|
|||
className={
|
||||
showNode
|
||||
? "generic-node-desc " +
|
||||
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
|
||||
(data.node?.description !== "" ? "py-5" : "pb-5")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{data.node?.description !== "" && showNode && (
|
||||
<div className="generic-node-desc-text">
|
||||
{data.node?.description !== "" &&
|
||||
showNode &&
|
||||
data.node?.flow &&
|
||||
inputDescription ? (
|
||||
<Textarea
|
||||
autoFocus
|
||||
onBlur={() => {
|
||||
setInputDescription(false);
|
||||
if (nodeDescription.trim() !== "") {
|
||||
setNodeDescription(nodeDescription);
|
||||
data.node!.description = nodeDescription;
|
||||
} else {
|
||||
setNodeDescription(data.node!.description);
|
||||
}
|
||||
}}
|
||||
value={nodeDescription}
|
||||
onChange={(e) => setNodeDescription(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, nodeDescription, "");
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
e.shiftKey === false &&
|
||||
e.ctrlKey === false &&
|
||||
e.altKey === false
|
||||
) {
|
||||
setInputDescription(false);
|
||||
if (nodeDescription.trim() !== "") {
|
||||
setNodeDescription(nodeDescription);
|
||||
data.node!.description = nodeDescription;
|
||||
} else {
|
||||
setNodeDescription(data.node!.description);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="generic-node-desc-text"
|
||||
onDoubleClick={() => setInputDescription(true)}
|
||||
>
|
||||
{data.node?.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<>
|
||||
{Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.sort()
|
||||
.map((templateField: string, idx) => (
|
||||
<div key={idx}>
|
||||
{data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced ? (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node!.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
index={idx.toString()}
|
||||
key={scapedJSONStringfy({
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
|
|
@ -368,15 +443,10 @@ export default function GenericNode({
|
|||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node?.template[templateField].display_name
|
||||
? data.node.template[templateField].display_name
|
||||
: data.node?.template[templateField].name
|
||||
? toTitleCase(
|
||||
data.node.template[templateField].name
|
||||
)
|
||||
: toTitleCase(templateField)
|
||||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
tooltipTitle={
|
||||
|
|
@ -384,21 +454,20 @@ export default function GenericNode({
|
|||
"\n"
|
||||
) ?? data.node?.template[templateField].type
|
||||
}
|
||||
required={data.node?.template[templateField].required}
|
||||
id={
|
||||
(data.node?.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
required={data.node!.template[templateField].required}
|
||||
id={{
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
}}
|
||||
left={true}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -415,7 +484,11 @@ export default function GenericNode({
|
|||
{" "}
|
||||
</div>
|
||||
<ParameterComponent
|
||||
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
||||
key={scapedJSONStringfy({
|
||||
baseClasses: data.node!.base_classes,
|
||||
id: data.id,
|
||||
dataType: data.type,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
|
|
@ -425,7 +498,11 @@ export default function GenericNode({
|
|||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
||||
id={{
|
||||
baseClasses: data.node!.base_classes,
|
||||
id: data.id,
|
||||
dataType: data.type,
|
||||
}}
|
||||
type={data.node?.base_classes.join("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default function DropdownButton({
|
|||
<DropdownMenu open={showOptions}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
id="new-project-btn"
|
||||
variant="primary"
|
||||
className="relative pr-10"
|
||||
onClick={(event) => {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
type="text"
|
||||
name="name"
|
||||
value={name ?? ""}
|
||||
placeholder="File name"
|
||||
placeholder="Flow name"
|
||||
id="name"
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { cardComponentPropsType } from "../../types/components";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
|
@ -17,7 +17,7 @@ export const CardComponent = ({
|
|||
onDelete,
|
||||
button,
|
||||
}: cardComponentPropsType): JSX.Element => {
|
||||
const { removeFlow } = useContext(TabsContext);
|
||||
const { removeFlow } = useContext(FlowsContext);
|
||||
|
||||
return (
|
||||
<Card className="group">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { typesContext } from "../../../contexts/typesContext";
|
|||
import { postBuildInit } from "../../../controllers/API";
|
||||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../contexts/flowsContext";
|
||||
import { parsedDataType } from "../../../types/components";
|
||||
import { TabsState } from "../../../types/tabs";
|
||||
import { validateNodes } from "../../../utils/reactflowUtils";
|
||||
|
|
@ -26,7 +26,7 @@ export default function BuildTrigger({
|
|||
}): JSX.Element {
|
||||
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setTabsState } = useContext(TabsContext);
|
||||
const { setTabsState } = useContext(FlowsContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const [isIconTouched, setIsIconTouched] = useState(false);
|
||||
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||
|
|
@ -80,61 +80,54 @@ export default function BuildTrigger({
|
|||
const { flowId } = response.data;
|
||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||
let validationResults: boolean[] = [];
|
||||
let finished = false;
|
||||
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
// Close the connection and finish
|
||||
finished = true;
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
eventSource.close();
|
||||
resolve(validationResults.every((result) => result));
|
||||
} else if (parsedData.log) {
|
||||
// If the event is a log, log it
|
||||
setSuccessData({ title: parsedData.log });
|
||||
} else if (parsedData.input_keys !== undefined) {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
return {
|
||||
...old,
|
||||
[flowId]: {
|
||||
...old[flowId],
|
||||
formKeysData: parsedData,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Otherwise, process the data
|
||||
const isValid = processStreamResult(parsedData);
|
||||
setProgress(parsedData.progress);
|
||||
validationResults.push(isValid);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error: any) => {
|
||||
console.error("EventSource failed:", error);
|
||||
|
||||
if (error.data) {
|
||||
const parsedData = JSON.parse(error.data);
|
||||
setErrorData({ title: parsedData.error });
|
||||
setIsBuilding(false);
|
||||
}
|
||||
eventSource.close();
|
||||
|
||||
return;
|
||||
} else if (parsedData.log) {
|
||||
// If the event is a log, log it
|
||||
setSuccessData({ title: parsedData.log });
|
||||
} else if (parsedData.input_keys !== undefined) {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
return {
|
||||
...old,
|
||||
[flowId]: {
|
||||
...old[flowId],
|
||||
formKeysData: parsedData,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Otherwise, process the data
|
||||
const isValid = processStreamResult(parsedData);
|
||||
setProgress(parsedData.progress);
|
||||
validationResults.push(isValid);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error: any) => {
|
||||
console.error("EventSource failed:", error);
|
||||
eventSource.close();
|
||||
if (error.data) {
|
||||
const parsedData = JSON.parse(error.data);
|
||||
setErrorData({ title: parsedData.error });
|
||||
setIsBuilding(false);
|
||||
}
|
||||
};
|
||||
// Step 3: Wait for the stream to finish
|
||||
while (!finished) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
finished = validationResults.length === flow.data!.nodes.length;
|
||||
}
|
||||
// Step 4: Return true if all nodes are valid, false otherwise
|
||||
return validationResults.every((result) => result);
|
||||
reject(new Error("Streaming failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function processStreamResult(parsedData: parsedDataType) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import BuildTrigger from "./buildTrigger";
|
|||
import ChatTrigger from "./chatTrigger";
|
||||
|
||||
import * as _ from "lodash";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import FormModal from "../../modals/formModal";
|
||||
import { NodeType } from "../../types/flow";
|
||||
|
|
@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow";
|
|||
export default function Chat({ flow }: ChatType): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [canOpen, setCanOpen] = useState(false);
|
||||
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
|
||||
const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export default function CodeAreaComponent({
|
|||
nodeClass,
|
||||
dynamic,
|
||||
setNodeClass,
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: CodeAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(
|
||||
typeof value == "string" ? value : JSON.stringify(value)
|
||||
|
|
@ -30,6 +32,7 @@ export default function CodeAreaComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<CodeAreaModal
|
||||
readonly={readonly}
|
||||
dynamic={dynamic}
|
||||
value={myValue}
|
||||
nodeClass={nodeClass}
|
||||
|
|
@ -41,6 +44,7 @@ export default function CodeAreaComponent({
|
|||
>
|
||||
<div className="flex w-full items-center">
|
||||
<span
|
||||
id={id}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node input-dialog"
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default function CodeTabsComponent({
|
|||
}, [flow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tweaks) {
|
||||
if (tweaks && data) {
|
||||
unselectAllNodes({
|
||||
data,
|
||||
updateNodes: (nodes) => {
|
||||
|
|
@ -604,6 +604,14 @@ export default function CodeTabsComponent({
|
|||
].type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
node.data.node?.flow &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={
|
||||
|
|
@ -646,6 +654,14 @@ export default function CodeTabsComponent({
|
|||
<CodeAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
readonly={
|
||||
node.data.node?.flow &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export default function DictComponent({
|
|||
}, [value]);
|
||||
|
||||
const ref = useRef(value);
|
||||
debugger;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function FloatComponent({
|
|||
return (
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id="float-input"
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -17,7 +17,7 @@ import IconComponent from "../../../genericIconComponent";
|
|||
import { Button } from "../../../ui/button";
|
||||
|
||||
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
||||
const { addFlow } = useContext(TabsContext);
|
||||
const { addFlow } = useContext(FlowsContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { undo, redo } = useContext(undoRedoContext);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
|
|
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
|||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow(undefined, true).then((id) => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
|||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Button } from "../ui/button";
|
||||
|
|
@ -22,7 +22,7 @@ import { Separator } from "../ui/separator";
|
|||
import MenuBar from "./components/menuBar";
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const { flows, tabId } = useContext(TabsContext);
|
||||
const { flows, tabId } = useContext(FlowsContext);
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter } = useContext(alertContext);
|
||||
const location = useLocation();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { InputComponentType } from "../../types/components";
|
||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export default function InputComponent({
|
||||
autoFocus = false,
|
||||
onBlur,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
|
|
@ -15,9 +17,11 @@ export default function InputComponent({
|
|||
editNode = false,
|
||||
placeholder = "Type something...",
|
||||
className,
|
||||
id = "",
|
||||
blurOnEnter = false,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -30,6 +34,10 @@ export default function InputComponent({
|
|||
{isForm ? (
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
id={"form-" + id}
|
||||
ref={refInput}
|
||||
onBlur={onBlur}
|
||||
autoFocus={autoFocus}
|
||||
type={password && !pwdVisible ? "password" : "text"}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
|
|
@ -49,13 +57,18 @@ export default function InputComponent({
|
|||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={classNames(
|
||||
|
|
@ -73,6 +86,7 @@ export default function InputComponent({
|
|||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { uploadFile } from "../../controllers/API";
|
||||
import { FileComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
|
@ -17,7 +17,7 @@ export default function InputFileComponent({
|
|||
const [myValue, setMyValue] = useState(value);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { tabId } = useContext(TabsContext);
|
||||
const { tabId } = useContext(FlowsContext);
|
||||
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default function IntComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
id = "",
|
||||
}: FloatComponentType): JSX.Element {
|
||||
const min = 0;
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ export default function IntComponent({
|
|||
return (
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id={id}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key !== "Backspace" &&
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export default function KeypairListComponent({
|
|||
return (
|
||||
<div key={idx} className="flex w-full gap-2">
|
||||
<Input
|
||||
id={"keypair" + index}
|
||||
type="text"
|
||||
value={key.trim()}
|
||||
className={classNames(
|
||||
|
|
@ -72,6 +73,7 @@ export default function KeypairListComponent({
|
|||
/>
|
||||
|
||||
<Input
|
||||
id={"keypair" + (index + 100).toString()}
|
||||
type="text"
|
||||
value={obj[key]}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
|
|
@ -88,6 +90,7 @@ export default function KeypairListComponent({
|
|||
newInputList.push({ "": "" });
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={"plusbtn" + index.toString()}
|
||||
>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
|
|
@ -101,6 +104,7 @@ export default function KeypairListComponent({
|
|||
newInputList.splice(index, 1);
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={"minusbtn" + index.toString()}
|
||||
>
|
||||
<IconComponent
|
||||
name="X"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ export default function PromptAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: PromptAreaComponentType) {
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: PromptAreaComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
onChange("");
|
||||
|
|
@ -22,7 +24,8 @@ export default function PromptAreaComponent({
|
|||
}, [disabled]);
|
||||
|
||||
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) => {
|
||||
if (apiReturn.data) {
|
||||
setNodeClass!(apiReturn.data.frontend_node);
|
||||
|
|
@ -35,6 +38,8 @@ export default function PromptAreaComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<GenericModal
|
||||
id={id}
|
||||
readonly={readonly}
|
||||
type={TypeModal.PROMPT}
|
||||
value={value}
|
||||
buttonText="Check & Save"
|
||||
|
|
@ -47,6 +52,7 @@ export default function PromptAreaComponent({
|
|||
>
|
||||
<div className="flex w-full items-center">
|
||||
<span
|
||||
id={id}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node input-dialog"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export default function TextAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
id = "",
|
||||
}: TextAreaComponentType): JSX.Element {
|
||||
// Clear text area
|
||||
useEffect(() => {
|
||||
|
|
@ -21,6 +22,7 @@ export default function TextAreaComponent({
|
|||
return (
|
||||
<div className="flex w-full items-center">
|
||||
<Input
|
||||
id={id}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export default function ToggleShadComponent({
|
|||
setEnabled,
|
||||
disabled,
|
||||
size,
|
||||
id = "",
|
||||
}: ToggleComponentType): JSX.Element {
|
||||
let scaleX, scaleY;
|
||||
switch (size) {
|
||||
|
|
@ -29,6 +30,7 @@ export default function ToggleShadComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
||||
<Switch
|
||||
id={id}
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
"z-50 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AxiosError } from "axios";
|
||||
import _ from "lodash";
|
||||
import _, { cloneDeep } from "lodash";
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
|
|
@ -21,10 +21,20 @@ import {
|
|||
} from "../controllers/API";
|
||||
import { APIClassType, APITemplateType } from "../types/api";
|
||||
import { tweakType } from "../types/components";
|
||||
import { FlowType, NodeDataType, NodeType } from "../types/flow";
|
||||
import { TabsContextType, TabsState } from "../types/tabs";
|
||||
import {
|
||||
FlowType,
|
||||
NodeDataType,
|
||||
NodeType,
|
||||
sourceHandleType,
|
||||
targetHandleType,
|
||||
} from "../types/flow";
|
||||
import { FlowsContextType, TabsState } from "../types/tabs";
|
||||
import {
|
||||
addVersionToDuplicates,
|
||||
checkOldEdgesHandles,
|
||||
scapeJSONParse,
|
||||
scapedJSONStringfy,
|
||||
updateEdgesHandleIds,
|
||||
updateIds,
|
||||
updateTemplate,
|
||||
} from "../utils/reactflowUtils";
|
||||
|
|
@ -35,13 +45,13 @@ import { typesContext } from "./typesContext";
|
|||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
const TabsContextInitialValue: TabsContextType = {
|
||||
const FlowsContextInitialValue: FlowsContextType = {
|
||||
tabId: "",
|
||||
setTabId: (index: string) => {},
|
||||
isLoading: true,
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: async (flowData?: any) => "",
|
||||
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => uid(),
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
|
|
@ -65,8 +75,8 @@ const TabsContextInitialValue: TabsContextType = {
|
|||
) => {},
|
||||
};
|
||||
|
||||
export const TabsContext = createContext<TabsContextType>(
|
||||
TabsContextInitialValue
|
||||
export const FlowsContext = createContext<FlowsContextType>(
|
||||
FlowsContextInitialValue
|
||||
);
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
|
|
@ -134,14 +144,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
if (!flow.data) {
|
||||
return;
|
||||
}
|
||||
processFlowEdges(flow);
|
||||
processFlowNodes(flow);
|
||||
processDataFromFlow(flow, false);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
function processFlowEdges(flow: FlowType) {
|
||||
if (!flow.data || !flow.data.edges) return;
|
||||
if (checkOldEdgesHandles(flow.data.edges)) {
|
||||
const newEdges = updateEdgesHandleIds(flow.data);
|
||||
flow.data.edges = newEdges;
|
||||
}
|
||||
//update edges colors
|
||||
flow.data.edges.forEach((edge) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555" };
|
||||
|
|
@ -159,6 +173,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
function processFlowNodes(flow: FlowType) {
|
||||
if (!flow.data || !flow.data.nodes) return;
|
||||
flow.data.nodes.forEach((node: NodeType) => {
|
||||
if (node.data.node?.flow) return;
|
||||
if (skipNodeUpdate.includes(node.data.type)) return;
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
|
|
@ -168,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
if (Object.keys(template["template"]).length > 0) {
|
||||
updateDisplay_name(node, template);
|
||||
updateNodeBaseClasses(node, template);
|
||||
//update baseclasses in edges
|
||||
updateNodeEdges(flow, node, template);
|
||||
updateNodeDescription(node, template);
|
||||
updateNodeTemplate(node, template);
|
||||
|
|
@ -187,11 +203,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
) {
|
||||
flow.data!.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
?.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
sourceHandleObject.baseClasses = template["base_classes"];
|
||||
edge.data.sourceHandle = sourceHandleObject;
|
||||
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -243,9 +260,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
setNoticeData({
|
||||
title: "Warning: Critical data, JSON file may include API keys.",
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFlows() {
|
||||
|
|
@ -274,7 +288,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* The resulting JSON object is passed to the addFlow function.
|
||||
*/
|
||||
async function uploadFlow(
|
||||
newProject?: boolean,
|
||||
newProject: boolean,
|
||||
file?: File
|
||||
): Promise<String | undefined> {
|
||||
let id;
|
||||
|
|
@ -283,13 +297,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
let fileData = JSON.parse(text);
|
||||
if (fileData.flows) {
|
||||
fileData.flows.forEach((flow: FlowType) => {
|
||||
id = addFlow(flow, newProject);
|
||||
id = addFlow(newProject, flow);
|
||||
});
|
||||
}
|
||||
// parse the text into a JSON object
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
|
||||
id = await addFlow(flow, newProject);
|
||||
id = await addFlow(newProject, flow);
|
||||
} else {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
|
|
@ -304,7 +318,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
const currentfile = (e.target as HTMLInputElement).files![0];
|
||||
let text = await currentfile.text();
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
const flowId = await addFlow(flow, newProject);
|
||||
const flowId = await addFlow(newProject, flow);
|
||||
resolve(flowId);
|
||||
}
|
||||
};
|
||||
|
|
@ -355,7 +369,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* Add a new flow to the list of flows.
|
||||
* @param flow Optional flow to add.
|
||||
*/
|
||||
|
||||
function paste(
|
||||
selectionInstance: { nodes: Node[]; edges: Edge[] },
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
|
|
@ -404,19 +417,28 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
reactFlowInstance!.setNodes(nodes);
|
||||
|
||||
selectionInstance.edges.forEach((edge) => {
|
||||
selectionInstance.edges.forEach((edge: Edge) => {
|
||||
let source = idsMap[edge.source];
|
||||
let target = idsMap[edge.target];
|
||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
||||
let sourceHandle =
|
||||
sourceHandleSplitted[0] +
|
||||
"|" +
|
||||
source +
|
||||
"|" +
|
||||
sourceHandleSplitted.slice(2).join("|");
|
||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
||||
let targetHandle =
|
||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
let sourceHandle = scapedJSONStringfy({
|
||||
...sourceHandleObject,
|
||||
id: source,
|
||||
});
|
||||
sourceHandleObject.id = source;
|
||||
|
||||
edge.data.sourceHandle = sourceHandleObject;
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
let targetHandle = scapedJSONStringfy({
|
||||
...targetHandleObject,
|
||||
id: target,
|
||||
});
|
||||
targetHandleObject.id = target;
|
||||
edge.data.targetHandle = targetHandleObject;
|
||||
let id =
|
||||
"reactflow__edge-" +
|
||||
source +
|
||||
|
|
@ -431,12 +453,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
sourceHandle,
|
||||
targetHandle,
|
||||
id,
|
||||
data: cloneDeep(edge.data),
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
targetHandle.split("|")[0] === "Text"
|
||||
targetHandleObject.type === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ",
|
||||
animated: targetHandle.split("|")[0] === "Text",
|
||||
animated: targetHandleObject.type === "Text",
|
||||
selected: false,
|
||||
},
|
||||
edges.map((edge) => ({ ...edge, selected: false }))
|
||||
|
|
@ -446,19 +469,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
const addFlow = async (
|
||||
flow?: FlowType,
|
||||
newProject?: Boolean
|
||||
newProject: Boolean,
|
||||
flow?: FlowType
|
||||
): Promise<String | undefined> => {
|
||||
if (newProject) {
|
||||
let flowData = extractDataFromFlow(flow!);
|
||||
if (flowData.description == "") {
|
||||
flowData.description = getRandomDescription();
|
||||
}
|
||||
let flowData = flow
|
||||
? processDataFromFlow(flow)
|
||||
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
const newFlow = createNewFlow(flowData, flow!);
|
||||
processFlowEdges(newFlow);
|
||||
processFlowNodes(newFlow);
|
||||
|
||||
const flowName = addVersionToDuplicates(newFlow, flows);
|
||||
|
||||
|
|
@ -486,31 +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;
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
if (data) {
|
||||
processFlowEdges(flow);
|
||||
//prevent node update for now
|
||||
// processFlowNodes(flow);
|
||||
//add animation to text type edges
|
||||
updateEdges(data.edges);
|
||||
updateNodes(data.nodes, data.edges);
|
||||
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
// updateNodes(data.nodes, data.edges);
|
||||
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
}
|
||||
|
||||
return { data, description };
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateEdges = (edges: Edge[]) => {
|
||||
edges.forEach((edge) => {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
edge.className =
|
||||
(edge.targetHandle!.split("|")[0] === "Text"
|
||||
(targetHandleObject.type === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ") + " stroke-connection";
|
||||
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
|
||||
edge.animated = targetHandleObject.type === "Text";
|
||||
});
|
||||
};
|
||||
|
||||
const updateNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.node?.flow) return;
|
||||
if (skipNodeUpdate.includes(node.data.type)) return;
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
|
|
@ -520,12 +546,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
if (Object.keys(template["template"]).length > 0) {
|
||||
node.data.node.base_classes = template["base_classes"];
|
||||
edges.forEach((edge) => {
|
||||
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge
|
||||
.sourceHandle!.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
let newSourceHandle = sourceHandleObject;
|
||||
newSourceHandle.baseClasses.concat(template["base_classes"]);
|
||||
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||
}
|
||||
});
|
||||
node.data.node.description = template["description"];
|
||||
|
|
@ -538,12 +565,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
};
|
||||
|
||||
const createNewFlow = (
|
||||
flowData: { data: ReactFlowJsonObject | null; description: string },
|
||||
flowData: ReactFlowJsonObject | null,
|
||||
flow: FlowType
|
||||
) => ({
|
||||
description: flowData.description,
|
||||
description: flow?.description ?? getRandomDescription(),
|
||||
name: flow?.name ?? getRandomName(),
|
||||
data: flowData.data,
|
||||
data: flowData,
|
||||
id: "",
|
||||
});
|
||||
|
||||
|
|
@ -611,7 +638,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
const [isBuilt, setIsBuilt] = useState(false);
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
<FlowsContext.Provider
|
||||
value={{
|
||||
saveFlow,
|
||||
isBuilt,
|
||||
|
|
@ -640,6 +667,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
</FlowsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import { SSEProvider } from "./SSEContext";
|
|||
import { AlertProvider } from "./alertContext";
|
||||
import { AuthProvider } from "./authContext";
|
||||
import { DarkProvider } from "./darkContext";
|
||||
import { TabsProvider } from "./flowsContext";
|
||||
import { LocationProvider } from "./locationContext";
|
||||
import { TabsProvider } from "./tabsContext";
|
||||
import { TypesProvider } from "./typesContext";
|
||||
import { UndoRedoProvider } from "./undoRedoContext";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
undoRedoContextType,
|
||||
} from "../types/typesContext";
|
||||
import { isWrappedWithClass } from "../utils/utils";
|
||||
import { TabsContext } from "./tabsContext";
|
||||
import { FlowsContext } from "./flowsContext";
|
||||
|
||||
const initialValue = {
|
||||
undo: () => {},
|
||||
|
|
@ -29,7 +29,7 @@ const defaultOptions: UseUndoRedoOptions = {
|
|||
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
|
||||
|
||||
export function UndoRedoProvider({ children }) {
|
||||
const { tabId, flows } = useContext(TabsContext);
|
||||
const { tabId, flows } = useContext(FlowsContext);
|
||||
|
||||
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import CodeTabsComponent from "../../components/codeTabsComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { EXPORT_CODE_DIALOG } from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
import { tweakType, uniqueTweakType } from "../../types/components";
|
||||
import { FlowType, NodeType } from "../../types/flow/index";
|
||||
|
|
@ -45,7 +45,7 @@ const ApiModal = forwardRef(
|
|||
const [activeTab, setActiveTab] = useState("0");
|
||||
const tweak = useRef<tweakType>([]);
|
||||
const tweaksList = useRef<string[]>([]);
|
||||
const { setTweak, getTweak, tabsState } = useContext(TabsContext);
|
||||
const { setTweak, getTweak, tabsState } = useContext(FlowsContext);
|
||||
const pythonApiCode = getPythonApiCode(
|
||||
flow,
|
||||
autoLogin,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import DictComponent from "../../components/dictComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
|
|
@ -31,7 +33,7 @@ import {
|
|||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { limitScrollFieldsModal } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { TabsState } from "../../types/tabs";
|
||||
|
|
@ -63,10 +65,11 @@ const EditNodeModal = forwardRef(
|
|||
ref
|
||||
) => {
|
||||
const [modalOpen, setModalOpen] = useState(open ?? false);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const myData = useRef(data);
|
||||
|
||||
const { setTabsState, tabId } = useContext(TabsContext);
|
||||
const { setTabsState, tabId } = useContext(FlowsContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance
|
||||
|
|
@ -82,11 +85,14 @@ const EditNodeModal = forwardRef(
|
|||
const handleOnNewValue = (newValue: any, name) => {
|
||||
myData.current.node!.template[name].value = newValue;
|
||||
setDataValue(newValue);
|
||||
updateNodeInternals(data.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
myData.current = data; // reset data to what it is on node when opening modal
|
||||
onClose!(modalOpen);
|
||||
if (modalOpen) {
|
||||
myData.current = data; // reset data to what it is on node when opening modal
|
||||
onClose!(modalOpen);
|
||||
}
|
||||
}, [modalOpen]);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
|
@ -166,11 +172,27 @@ const EditNodeModal = forwardRef(
|
|||
.map((templateParam, index) => (
|
||||
<TableRow key={index} className="h-10">
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
{myData.current.node?.template[templateParam].name
|
||||
? myData.current.node.template[templateParam]
|
||||
.name
|
||||
: myData.current.node?.template[templateParam]
|
||||
.display_name}
|
||||
<ShadTooltip
|
||||
content={
|
||||
myData.current.node?.template[templateParam]
|
||||
.proxy
|
||||
? myData.current.node?.template[
|
||||
templateParam
|
||||
].proxy?.id
|
||||
: null
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{myData.current.node?.template[templateParam]
|
||||
.display_name
|
||||
? myData.current.node.template[
|
||||
templateParam
|
||||
].display_name
|
||||
: myData.current.node?.template[
|
||||
templateParam
|
||||
].name}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||
{myData.current.node?.template[templateParam]
|
||||
|
|
@ -203,6 +225,7 @@ const EditNodeModal = forwardRef(
|
|||
templateParam
|
||||
].multiline ? (
|
||||
<TextAreaComponent
|
||||
id={"textarea-edit-" + index}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
|
|
@ -216,6 +239,7 @@ const EditNodeModal = forwardRef(
|
|||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={"input-" + index}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
password={
|
||||
|
|
@ -311,6 +335,7 @@ const EditNodeModal = forwardRef(
|
|||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleShadComponent
|
||||
id={"toggle-edit-" + index}
|
||||
disabled={disabled}
|
||||
enabled={
|
||||
myData.current.node.template[
|
||||
|
|
@ -369,6 +394,7 @@ const EditNodeModal = forwardRef(
|
|||
.type === "int" ? (
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
id={"int-input-" + index}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
|
|
@ -416,6 +442,9 @@ const EditNodeModal = forwardRef(
|
|||
.type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
myData.current.node?.flow ? true : false
|
||||
}
|
||||
field_name={templateParam}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
|
|
@ -431,12 +460,21 @@ const EditNodeModal = forwardRef(
|
|||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={"prompt-area-edit" + index}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
myData.current.node?.flow &&
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={
|
||||
data.node!.template[templateParam]
|
||||
.dynamic ?? false
|
||||
|
|
@ -455,6 +493,7 @@ const EditNodeModal = forwardRef(
|
|||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={"code-area-edit" + index}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
|
|
@ -467,6 +506,11 @@ const EditNodeModal = forwardRef(
|
|||
<TableCell className="p-0 text-right">
|
||||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
id={
|
||||
"show" +
|
||||
myData.current.node?.template[templateParam]
|
||||
.name
|
||||
}
|
||||
enabled={
|
||||
!myData.current.node?.template[
|
||||
templateParam
|
||||
|
|
@ -492,6 +536,7 @@ const EditNodeModal = forwardRef(
|
|||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
id={"saveChangesBtn"}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
const newData = cloneDeep(myData.current);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from "react";
|
|||
import AceEditor from "react-ace";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
|
|
@ -23,6 +24,7 @@ export default function CodeAreaModal({
|
|||
setNodeClass,
|
||||
children,
|
||||
dynamic,
|
||||
readonly = false,
|
||||
}: codeAreaModalPropsType): JSX.Element {
|
||||
const [code, setCode] = useState(value);
|
||||
const { dark } = useContext(darkContext);
|
||||
|
|
@ -143,9 +145,15 @@ export default function CodeAreaModal({
|
|||
/>
|
||||
</BaseModal.Header>
|
||||
<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="h-full w-full">
|
||||
<AceEditor
|
||||
readOnly={readonly}
|
||||
value={code}
|
||||
mode="python"
|
||||
height={height ?? "100%"}
|
||||
|
|
@ -180,7 +188,13 @@ export default function CodeAreaModal({
|
|||
</div>
|
||||
</div>
|
||||
<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
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ import IconComponent from "../../components/genericIconComponent";
|
|||
import { Button } from "../../components/ui/button";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
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 BaseModal from "../baseModal";
|
||||
|
||||
const ExportModal = forwardRef(
|
||||
(props: { children: ReactNode }, ref): JSX.Element => {
|
||||
const { flows, tabId, downloadFlow } = useContext(TabsContext);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const { flows, tabId, downloadFlow } = useContext(FlowsContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setNoticeData } = useContext(alertContext);
|
||||
const [checked, setChecked] = useState(true);
|
||||
const flow = flows.find((f) => f.id === tabId);
|
||||
useEffect(() => {
|
||||
setName(flow!.name);
|
||||
|
|
@ -44,6 +48,7 @@ const ExportModal = forwardRef(
|
|||
<div className="mt-3 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
checked={checked}
|
||||
onCheckedChange={(event: boolean) => {
|
||||
setChecked(event);
|
||||
}}
|
||||
|
|
@ -52,20 +57,38 @@ const ExportModal = forwardRef(
|
|||
Save with my API keys
|
||||
</label>
|
||||
</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.Footer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (checked)
|
||||
if (checked) {
|
||||
downloadFlow(
|
||||
flows.find((flow) => flow.id === tabId)!,
|
||||
{
|
||||
id: tabId,
|
||||
data: reactFlowInstance?.toObject()!,
|
||||
description,
|
||||
name,
|
||||
},
|
||||
name!,
|
||||
description
|
||||
);
|
||||
else
|
||||
setNoticeData({
|
||||
title:
|
||||
"Warning: Critical data, JSON file may include API keys.",
|
||||
});
|
||||
} else
|
||||
downloadFlow(
|
||||
removeApiKeys(flows.find((flow) => flow.id === tabId)!),
|
||||
removeApiKeys({
|
||||
id: tabId,
|
||||
data: reactFlowInstance?.toObject()!,
|
||||
description,
|
||||
name,
|
||||
}),
|
||||
name!,
|
||||
description
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { FlowSettingsPropsType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export default function FlowSettingsModal({
|
|||
open,
|
||||
setOpen,
|
||||
}: 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);
|
||||
useEffect(() => {
|
||||
setName(flow!.name);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import { TabsState } from "../../types/tabs";
|
||||
import { validateNodes } from "../../utils/reactflowUtils";
|
||||
|
|
@ -38,7 +38,7 @@ export default function FormModal({
|
|||
setOpen: (open: boolean) => void;
|
||||
flow: FlowType;
|
||||
}): JSX.Element {
|
||||
const { tabsState, setTabsState } = useContext(TabsContext);
|
||||
const { tabsState, setTabsState } = useContext(FlowsContext);
|
||||
const [chatValue, setChatValue] = useState(() => {
|
||||
try {
|
||||
const { formKeysData } = tabsState[flow.id];
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export default function GenericModal({
|
|||
nodeClass,
|
||||
setNodeClass,
|
||||
children,
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: genericModalPropsType): JSX.Element {
|
||||
const [myButtonText] = useState(buttonText);
|
||||
const [myModalTitle] = useState(modalTitle);
|
||||
|
|
@ -208,8 +210,9 @@ export default function GenericModal({
|
|||
"flex h-full w-full"
|
||||
)}
|
||||
>
|
||||
{type === TypeModal.PROMPT && isEdit ? (
|
||||
{type === TypeModal.PROMPT && isEdit && !readonly ? (
|
||||
<Textarea
|
||||
id={"modal-" + id}
|
||||
ref={divRefPrompt}
|
||||
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
|
||||
value={inputValue}
|
||||
|
|
@ -226,7 +229,7 @@ export default function GenericModal({
|
|||
handleKeyDown(e, inputValue, "");
|
||||
}}
|
||||
/>
|
||||
) : type === TypeModal.PROMPT && !isEdit ? (
|
||||
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
|
||||
<SanitizedHTMLWrapper
|
||||
className={getClassByNumberLength()}
|
||||
content={coloredContent}
|
||||
|
|
@ -248,6 +251,7 @@ export default function GenericModal({
|
|||
onKeyDown={(e) => {
|
||||
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"
|
||||
>
|
||||
<div className="relative bottom-[1px]">
|
||||
<span>
|
||||
<span id={"badge" + index.toString()}>
|
||||
{word.replace(/[{}]/g, "").length > 59
|
||||
? word.replace(/[{}]/g, "").slice(0, 56) +
|
||||
"..."
|
||||
|
|
@ -304,6 +308,8 @@ export default function GenericModal({
|
|||
)}
|
||||
</div>
|
||||
<Button
|
||||
id="genericModalBtnSave"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
switch (myModalType) {
|
||||
case TypeModal.TEXT:
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
import { buttonBoxPropsType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
|
||||
export default function ButtonBox({
|
||||
onClick,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
bgColor,
|
||||
textColor,
|
||||
deactivate,
|
||||
size,
|
||||
}: buttonBoxPropsType): JSX.Element {
|
||||
let bigCircle: string;
|
||||
let smallCircle: string;
|
||||
let titleFontSize: string;
|
||||
let descriptionFontSize: string;
|
||||
let padding: string;
|
||||
let marginTop: string;
|
||||
let height: string;
|
||||
let width: string;
|
||||
let textHeight: number;
|
||||
let textWidth: number;
|
||||
switch (size) {
|
||||
case "small":
|
||||
bigCircle = "h-12 w-12";
|
||||
smallCircle = "h-8 w-8";
|
||||
titleFontSize = "text-sm";
|
||||
descriptionFontSize = "text-xs";
|
||||
padding = "p-2 py-3";
|
||||
marginTop = "mt-2";
|
||||
height = "h-36";
|
||||
width = "w-32";
|
||||
break;
|
||||
case "medium":
|
||||
bigCircle = "h-16 w-16";
|
||||
smallCircle = "h-12 w-12";
|
||||
titleFontSize = "text-base";
|
||||
descriptionFontSize = "text-sm";
|
||||
padding = "p-4 py-5";
|
||||
marginTop = "mt-3";
|
||||
height = "h-44";
|
||||
width = "w-36";
|
||||
break;
|
||||
case "big":
|
||||
bigCircle = "h-20 w-20";
|
||||
smallCircle = "h-16 w-16";
|
||||
titleFontSize = "text-lg";
|
||||
descriptionFontSize = "text-sm";
|
||||
padding = "p-8 py-10";
|
||||
marginTop = "mt-6";
|
||||
height = "h-56";
|
||||
width = "w-44";
|
||||
break;
|
||||
default:
|
||||
bigCircle = "h-20 w-20";
|
||||
smallCircle = "h-16 w-16";
|
||||
titleFontSize = "text-lg";
|
||||
descriptionFontSize = "text-sm";
|
||||
padding = "p-8 py-10";
|
||||
marginTop = "mt-6";
|
||||
height = "h-56";
|
||||
width = "w-44";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<button disabled={deactivate} onClick={onClick}>
|
||||
<div
|
||||
className={classNames(
|
||||
"button-box-modal-div",
|
||||
bgColor,
|
||||
height,
|
||||
width,
|
||||
padding
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center ${bigCircle} mb-1 rounded-full bg-background/30`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-center ${smallCircle} rounded-full bg-background`}
|
||||
>
|
||||
<div className={textColor}>{icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-auto mt-auto w-full">
|
||||
<h3
|
||||
className={classNames(
|
||||
"w-full font-semibold text-background truncate-multiline word-break-break-word",
|
||||
titleFontSize,
|
||||
marginTop
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowUpTrayIcon,
|
||||
ComputerDesktopIcon,
|
||||
DocumentDuplicateIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import LoadingComponent from "../../components/loadingComponent";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { IMPORT_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { getExamples } from "../../controllers/API";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import ButtonBox from "./buttonBox";
|
||||
|
||||
export default function ImportModal(): JSX.Element {
|
||||
const [open, setOpen] = useState(true);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const ref = useRef();
|
||||
const [showExamples, setShowExamples] = useState(false);
|
||||
const [loadingExamples, setLoadingExamples] = useState(false);
|
||||
const [examples, setExamples] = useState<FlowType[]>([]);
|
||||
const { uploadFlow, addFlow } = useContext(TabsContext);
|
||||
|
||||
function handleExamples(): void {
|
||||
setLoadingExamples(true);
|
||||
getExamples()
|
||||
.then((result) => {
|
||||
setLoadingExamples(false);
|
||||
setExamples(result);
|
||||
})
|
||||
.catch((error) =>
|
||||
setErrorData({
|
||||
title: "there was an error loading examples, please try again",
|
||||
list: [error.message],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent
|
||||
className={classNames(
|
||||
showExamples
|
||||
? "h-[600px] lg:max-w-[650px]"
|
||||
: "h-[450px] lg:max-w-[650px]"
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
{showExamples && (
|
||||
<>
|
||||
<div className="dialog-header-modal-div">
|
||||
<button
|
||||
type="button"
|
||||
className="dialog-header-modal-button disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
onClick={() => {
|
||||
setShowExamples(false);
|
||||
}}
|
||||
>
|
||||
<ArrowLeftIcon
|
||||
className="ml-1 h-5 w-5 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
|
||||
{showExamples ? "Select an example" : "Import"}
|
||||
</span>
|
||||
<ArrowUpTrayIcon
|
||||
className="ml-1 h-5 w-5 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
"dialog-modal-examples-div",
|
||||
showExamples && !loadingExamples
|
||||
? "dialog-modal-example-true"
|
||||
: "dialog-modal-example-false"
|
||||
)}
|
||||
>
|
||||
{!showExamples && (
|
||||
<div className="dialog-modal-button-box-div">
|
||||
<ButtonBox
|
||||
size="big"
|
||||
bgColor="bg-medium-emerald "
|
||||
description="Prebuilt Examples"
|
||||
icon={<DocumentDuplicateIcon className="document-icon" />}
|
||||
onClick={() => {
|
||||
setShowExamples(true);
|
||||
handleExamples();
|
||||
}}
|
||||
textColor="text-medium-emerald "
|
||||
title="Examples"
|
||||
></ButtonBox>
|
||||
<ButtonBox
|
||||
size="big"
|
||||
bgColor="bg-almost-dark-blue "
|
||||
description="Import from Local"
|
||||
icon={<ComputerDesktopIcon className="document-icon" />}
|
||||
onClick={() => {
|
||||
uploadFlow();
|
||||
setModalOpen(false);
|
||||
}}
|
||||
textColor="text-almost-dark-blue "
|
||||
title="Local File"
|
||||
></ButtonBox>
|
||||
</div>
|
||||
)}
|
||||
{showExamples && loadingExamples && (
|
||||
<div className="loading-component-div">
|
||||
<LoadingComponent remSize={30} />
|
||||
</div>
|
||||
)}
|
||||
{showExamples &&
|
||||
!loadingExamples &&
|
||||
examples.map((example, index) => {
|
||||
return (
|
||||
<div key={example.name} className="m-2">
|
||||
{" "}
|
||||
<ButtonBox
|
||||
size="small"
|
||||
bgColor="bg-medium-emerald "
|
||||
description={example.description ?? "Prebuilt Examples"}
|
||||
icon={
|
||||
<DocumentDuplicateIcon
|
||||
strokeWidth={1.5}
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
addFlow(example, false);
|
||||
setModalOpen(false);
|
||||
}}
|
||||
textColor="text-medium-emerald "
|
||||
title={example.name}
|
||||
></ButtonBox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="dialog-modal-footer">
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow_examples"
|
||||
target="_blank"
|
||||
className="dialog-modal-footer-link "
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
viewBox="0 0 98 96"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2 ">Langflow Examples</span>
|
||||
</a>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import {
|
||||
addUser,
|
||||
deleteUser,
|
||||
|
|
@ -44,7 +44,7 @@ export default function AdminPage() {
|
|||
const { userData } = useContext(AuthContext);
|
||||
const [totalRowsCount, setTotalRowsCount] = useState(0);
|
||||
|
||||
const { setTabId } = useContext(TabsContext);
|
||||
const { setTabId } = useContext(FlowsContext);
|
||||
|
||||
// set null id
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CardComponent } from "../../components/cardComponent";
|
||||
|
|
@ -12,7 +12,7 @@ import { getExamples } from "../../controllers/API";
|
|||
import { FlowType } from "../../types/flow";
|
||||
export default function CommunityPage(): JSX.Element {
|
||||
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
|
||||
useContext(TabsContext);
|
||||
useContext(FlowsContext);
|
||||
|
||||
// set null id
|
||||
useEffect(() => {
|
||||
|
|
@ -94,7 +94,7 @@ export default function CommunityPage(): JSX.Element {
|
|||
size="sm"
|
||||
className="whitespace-nowrap "
|
||||
onClick={() => {
|
||||
addFlow(flow, true).then((id) => {
|
||||
addFlow(true, flow).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default function DisclosureComponent({
|
|||
<div>
|
||||
<Disclosure.Button className="components-disclosure-arrangement">
|
||||
<div className="flex gap-4">
|
||||
{/* BUG ON THIS ICON */}
|
||||
<Icon strokeWidth={1.5} size={22} className="text-primary" />
|
||||
<span className="components-disclosure-title">{title}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,16 +28,23 @@ import ReactFlow, {
|
|||
import GenericNode from "../../../../CustomNodes/GenericNode";
|
||||
import Chat from "../../../../components/chatComponent";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import { locationContext } from "../../../../contexts/locationContext";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { FlowType, NodeType } from "../../../../types/flow";
|
||||
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
|
||||
import { TabsState } from "../../../../types/tabs";
|
||||
import { isValidConnection } from "../../../../utils/reactflowUtils";
|
||||
import { isWrappedWithClass } from "../../../../utils/utils";
|
||||
import {
|
||||
generateFlow,
|
||||
generateNodeFromFlow,
|
||||
isValidConnection,
|
||||
scapeJSONParse,
|
||||
validateSelection,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
import SelectionMenu from "../SelectionMenuComponent";
|
||||
import ExtraSidebar from "../extraSidebarComponent";
|
||||
|
||||
const nodeTypes = {
|
||||
|
|
@ -63,7 +70,7 @@ export default function Page({
|
|||
saveFlow,
|
||||
setTabsState,
|
||||
tabId,
|
||||
} = useContext(TabsContext);
|
||||
} = useContext(FlowsContext);
|
||||
const {
|
||||
types,
|
||||
reactFlowInstance,
|
||||
|
|
@ -238,12 +245,19 @@ export default function Page({
|
|||
addEdge(
|
||||
{
|
||||
...params,
|
||||
data: {
|
||||
targetHandle: scapeJSONParse(params.targetHandle!),
|
||||
sourceHandle: scapeJSONParse(params.sourceHandle!),
|
||||
},
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
(params.targetHandle?.split("|")[0] === "Text"
|
||||
((scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||
.type === "Text"
|
||||
? "stroke-foreground "
|
||||
: "stroke-foreground ") + " stroke-connection",
|
||||
animated: params.targetHandle?.split("|")[0] === "Text",
|
||||
animated:
|
||||
(scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||
.type === "Text",
|
||||
},
|
||||
eds
|
||||
)
|
||||
|
|
@ -296,7 +310,6 @@ export default function Page({
|
|||
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
|
||||
const position = reactFlowInstance!.project({
|
||||
x: event.clientX - reactflowBounds!.left,
|
||||
|
|
@ -394,7 +407,7 @@ export default function Page({
|
|||
edgeUpdateSuccessful.current = true;
|
||||
}, []);
|
||||
|
||||
const [selectionEnded, setSelectionEnded] = useState(false);
|
||||
const [selectionEnded, setSelectionEnded] = useState(true);
|
||||
|
||||
const onSelectionEnd = useCallback(() => {
|
||||
setSelectionEnded(true);
|
||||
|
|
@ -434,7 +447,7 @@ export default function Page({
|
|||
<div className="h-full w-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(templates).length > 0 &&
|
||||
Object.keys(types).length > 0 ? (
|
||||
<div className="h-full w-full">
|
||||
<div id="react-flow-id" className="h-full w-full">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onMove={() => {
|
||||
|
|
@ -481,6 +494,50 @@ export default function Page({
|
|||
[&>button]:border-b-border hover:[&>button]:bg-border"
|
||||
></Controls>
|
||||
)}
|
||||
<SelectionMenu
|
||||
isVisible={selectionMenuVisible}
|
||||
nodes={lastSelection?.nodes}
|
||||
onClick={() => {
|
||||
if (
|
||||
validateSelection(lastSelection!, edges).length === 0
|
||||
) {
|
||||
const { newFlow } = generateFlow(
|
||||
lastSelection!,
|
||||
reactFlowInstance!,
|
||||
getRandomName()
|
||||
);
|
||||
const newGroupNode = generateNodeFromFlow(
|
||||
newFlow,
|
||||
getNodeId
|
||||
);
|
||||
setNodes((oldNodes) => [
|
||||
...oldNodes.filter(
|
||||
(oldNodes) =>
|
||||
!lastSelection?.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldNodes.id
|
||||
)
|
||||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges((oldEdges) =>
|
||||
oldEdges.filter(
|
||||
(oldEdge) =>
|
||||
!lastSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Invalid selection",
|
||||
list: validateSelection(lastSelection!, edges),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ReactFlow>
|
||||
{!view && (
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const [lastNodes, setLastNodes] = useState(nodes);
|
||||
|
||||
// nodes get saved to not be gone after the toolbar closes
|
||||
useEffect(() => {
|
||||
setLastNodes(nodes);
|
||||
}, [isOpen]);
|
||||
|
||||
// transition starts after and ends before the toolbar closes
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
setIsOpen(true);
|
||||
setTimeout(() => {
|
||||
setIsTransitioning(true);
|
||||
}, 50);
|
||||
} else {
|
||||
setIsTransitioning(false);
|
||||
setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
}, 500);
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
return (
|
||||
<NodeToolbar
|
||||
isVisible={isOpen}
|
||||
offset={5}
|
||||
nodeId={
|
||||
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
|
||||
}
|
||||
>
|
||||
<div className="h-10 w-28 overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
"h-10 w-24 rounded-md border border-indigo-300 bg-white px-2.5 text-gray-700 shadow-inner transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300" +
|
||||
(isTransitioning ? " translate-y-0" : " translate-y-10")
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconComponent name="Group" className="w-6" />
|
||||
Group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</NodeToolbar>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import IconComponent from "../../../../components/genericIconComponent";
|
|||
import { Input } from "../../../../components/ui/input";
|
||||
import { Separator } from "../../../../components/ui/separator";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import ApiModal from "../../../../modals/ApiModal";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
|
|
@ -22,7 +22,7 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
const { data, templates, getFilterEdge, setFilterEdge } =
|
||||
useContext(typesContext);
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
|
||||
useContext(TabsContext);
|
||||
useContext(FlowsContext);
|
||||
const { setSuccessData, setErrorData } = useContext(alertContext);
|
||||
const [dataFilter, setFilterData] = useState(data);
|
||||
const [search, setSearch] = useState("");
|
||||
|
|
@ -136,7 +136,7 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
<button
|
||||
className="extra-side-bar-buttons"
|
||||
onClick={() => {
|
||||
uploadFlow();
|
||||
uploadFlow(false);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileUp" className="side-bar-button-size " />
|
||||
|
|
@ -271,7 +271,13 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
);
|
||||
}}
|
||||
>
|
||||
<div className="side-bar-components-div-form">
|
||||
<div
|
||||
className="side-bar-components-div-form"
|
||||
id={
|
||||
"side" +
|
||||
data[SBSectionName][SBItemName].display_name
|
||||
}
|
||||
>
|
||||
<span className="side-bar-components-text">
|
||||
{data[SBSectionName][SBItemName].display_name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -8,15 +8,20 @@ import {
|
|||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "../../../../components/ui/select-custom";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import EditNodeModal from "../../../../modals/EditNodeModal";
|
||||
import { nodeToolbarPropsType } from "../../../../types/components";
|
||||
import {
|
||||
expandGroupNode,
|
||||
updateFlowPosition,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
|
||||
|
||||
export default function NodeToolbarComponent({
|
||||
data,
|
||||
setData,
|
||||
deleteNode,
|
||||
position,
|
||||
setShowNode,
|
||||
numberOfHandles,
|
||||
showNode,
|
||||
|
|
@ -47,7 +52,9 @@ export default function NodeToolbarComponent({
|
|||
return true;
|
||||
}
|
||||
const isMinimal = canMinimize();
|
||||
const { paste } = useContext(TabsContext);
|
||||
const isGroup = data.node?.flow ? true : false;
|
||||
|
||||
const { paste } = useContext(FlowsContext);
|
||||
const reactFlowInstance = useReactFlow();
|
||||
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState("");
|
||||
|
|
@ -65,6 +72,10 @@ export default function NodeToolbarComponent({
|
|||
if (event.includes("disabled")) {
|
||||
return;
|
||||
}
|
||||
if (event.includes("ungroup")) {
|
||||
updateFlowPosition(position, data.node?.flow!);
|
||||
expandGroupNode(data, reactFlowInstance);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -134,11 +145,11 @@ export default function NodeToolbarComponent({
|
|||
</a>
|
||||
</ShadTooltip>
|
||||
|
||||
{isMinimal ? (
|
||||
{isMinimal || isGroup ? (
|
||||
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
||||
<ShadTooltip content="More" side="top">
|
||||
<SelectTrigger>
|
||||
<div>
|
||||
<div id="advancedIcon">
|
||||
<div
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
|
||||
|
|
@ -163,6 +174,7 @@ export default function NodeToolbarComponent({
|
|||
}
|
||||
>
|
||||
<div
|
||||
id="editAdvancedBtn"
|
||||
className={
|
||||
"flex " +
|
||||
(nodeLength == 0
|
||||
|
|
@ -179,7 +191,7 @@ export default function NodeToolbarComponent({
|
|||
</SelectItem>
|
||||
{isMinimal && (
|
||||
<SelectItem value={getRandomKeyByssmm() + "show"}>
|
||||
<div className="flex">
|
||||
<div className="flex" id="editAdvanced">
|
||||
<IconComponent
|
||||
name={showNode ? "Minimize2" : "Maximize2"}
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
|
|
@ -188,11 +200,22 @@ export default function NodeToolbarComponent({
|
|||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
{isGroup && (
|
||||
<SelectItem value={getRandomKeyByssmm() + "ungroup"}>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="Ungroup"
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>{" "}
|
||||
Ungroup{" "}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<div>
|
||||
<div id="editAdvancedIcon">
|
||||
<button
|
||||
disabled={nodeLength === 0}
|
||||
onClick={() => setShowModalAdvanced(true)}
|
||||
|
|
@ -222,29 +245,6 @@ export default function NodeToolbarComponent({
|
|||
<></>
|
||||
</EditNodeModal>
|
||||
)}
|
||||
|
||||
{/*
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<div>
|
||||
<EditNodeModal
|
||||
data={data}
|
||||
setData={setData}
|
||||
nodeLength={nodeLength}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
|
||||
(!canMinimize() && " rounded-r-md ") +
|
||||
(nodeLength == 0
|
||||
? " text-muted-foreground"
|
||||
: " text-foreground")
|
||||
)}
|
||||
>
|
||||
<IconComponent name="Settings2" className="h-4 w-4 " />
|
||||
</div>
|
||||
</EditNodeModal>
|
||||
</div>
|
||||
</ShadTooltip> */}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import Header from "../../components/headerComponent";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getVersion } from "../../controllers/API";
|
||||
import Page from "./components/PageComponent";
|
||||
|
||||
export default function FlowPage(): JSX.Element {
|
||||
const { flows, tabId, setTabId } = useContext(TabsContext);
|
||||
const { flows, tabId, setTabId } = useContext(FlowsContext);
|
||||
const { id } = useParams();
|
||||
|
||||
// Set flow tab id
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
|||
import { Button } from "../../components/ui/button";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
export default function HomePage(): JSX.Element {
|
||||
const {
|
||||
flows,
|
||||
|
|
@ -19,7 +19,7 @@ export default function HomePage(): JSX.Element {
|
|||
removeFlow,
|
||||
uploadFlow,
|
||||
isLoading,
|
||||
} = useContext(TabsContext);
|
||||
} = useContext(FlowsContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const dropdownOptions = [
|
||||
{
|
||||
|
|
@ -104,7 +104,7 @@ export default function HomePage(): JSX.Element {
|
|||
<DropdownButton
|
||||
firstButtonName="New Project"
|
||||
onFirstBtnClick={() => {
|
||||
addFlow(null!, true).then((id) => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Button } from "../../components/ui/button";
|
|||
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { resetPassword, updateUser } from "../../controllers/API";
|
||||
import {
|
||||
inputHandlerEventType,
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "../../types/components";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
export default function ProfileSettingsPage(): JSX.Element {
|
||||
const { setTabId } = useContext(TabsContext);
|
||||
const { setTabId } = useContext(FlowsContext);
|
||||
|
||||
const [inputState, setInputState] = useState<patchUserInputStateType>(
|
||||
CONTROL_PATCH_USER_STATE
|
||||
|
|
|
|||
|
|
@ -1,28 +1,18 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getVersion } from "../../controllers/API";
|
||||
import Page from "../FlowPage/components/PageComponent";
|
||||
|
||||
export default function ViewPage() {
|
||||
const { flows, tabId, setTabId } = useContext(TabsContext);
|
||||
const { setDark } = useContext(darkContext);
|
||||
const { id, theme } = useParams();
|
||||
const { flows, tabId, setTabId } = useContext(FlowsContext);
|
||||
const { id } = useParams();
|
||||
|
||||
// Set flow tab id
|
||||
useEffect(() => {
|
||||
setTabId(id!);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
setDark(theme === "dark");
|
||||
} else {
|
||||
setDark(false);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// Initialize state variable for the version
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const Router = () => {
|
|||
}
|
||||
/>
|
||||
<Route
|
||||
path="view/:theme?"
|
||||
path="view"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ViewPage />
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue