Merge dev into refactor/utils

This commit is contained in:
igorrCarvalho 2024-05-21 19:33:07 -03:00
commit 92c953cdcd
76 changed files with 2648 additions and 2456 deletions

View file

@ -4,6 +4,19 @@
# Do not commit .env file to git # Do not commit .env file to git
# Do not change .env.example file # Do not change .env.example file
# Config directory
# Directory where files, logs and database will be stored
# Example: LANGFLOW_CONFIG_DIR=~/.langflow
LANGFLOW_CONFIG_DIR=
# Save database in the config directory
# Values: true, false
# If false, the database will be saved in Langflow's root directory
# This means that the database will be deleted when Langflow is uninstalled
# and that the database will not be shared between different virtual environments
# Example: LANGFLOW_SAVE_DB_IN_CONFIG_DIR=true
LANGFLOW_SAVE_DB_IN_CONFIG_DIR=
# Database URL # Database URL
# Postgres example: LANGFLOW_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/langflow # Postgres example: LANGFLOW_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/langflow
# SQLite example: # SQLite example:
@ -56,7 +69,6 @@ LANGFLOW_REMOVE_API_KEYS=
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600) # LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
LANGFLOW_CACHE_TYPE= LANGFLOW_CACHE_TYPE=
# Set AUTO_LOGIN to false if you want to disable auto login # Set AUTO_LOGIN to false if you want to disable auto login
# and use the login form to login. LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD # and use the login form to login. LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD
# must be set if AUTO_LOGIN is set to false # must be set if AUTO_LOGIN is set to false

View file

@ -100,6 +100,12 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
## Deploy on Railway ## Deploy on Railway
Use this template to deploy Langflow 1.0 Preview on Railway:
[![Deploy 1.0 Preview on Railway](https://railway.app/button.svg)](https://railway.app/template/UsJ1uB?referralCode=MnPSdg)
Or this one to deploy Langflow 0.6.x:
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg) [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg)
## Deploy on Render ## Deploy on Render

View file

@ -10,7 +10,7 @@
# PYTHON-BASE # PYTHON-BASE
# Sets up all our shared environment variables # Sets up all our shared environment variables
################################ ################################
FROM python:3.10-slim as python-base FROM python:3.12-slim as python-base
# python # python
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
@ -47,7 +47,7 @@ ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
# Used to build deps + create our virtual environment # Used to build deps + create our virtual environment
################################ ################################
FROM python-base as builder-base FROM python-base as builder-base
RUN
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends -y \ && apt-get install --no-install-recommends -y \
# deps for installing poetry # deps for installing poetry
@ -55,7 +55,12 @@ RUN apt-get update \
# deps for building python deps # deps for building python deps
build-essential \ build-essential \
# npm # npm
npm npm \
# gcc
gcc \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Now we need to copy the entire project into the image # Now we need to copy the entire project into the image
@ -70,15 +75,12 @@ RUN --mount=type=cache,target=/root/.cache \
RUN python -m pip install requests && cd ./scripts && python update_dependencies.py RUN python -m pip install requests && cd ./scripts && python update_dependencies.py
RUN $POETRY_HOME/bin/poetry lock RUN $POETRY_HOME/bin/poetry lock
RUN $POETRY_HOME/bin/poetry build RUN $POETRY_HOME/bin/poetry build
# Final stage for the application
FROM python-base as final
# Copy virtual environment and built .tar.gz from builder base # Copy virtual environment and built .tar.gz from builder base
RUN useradd -m -u 1000 user RUN useradd -m -u 1000 user
COPY --from=builder-base /app/dist/*.tar.gz ./
# Install the package from the .tar.gz # Install the package from the .tar.gz
RUN python -m pip install *.tar.gz --user RUN python -m pip install /app/dist/*.tar.gz --user
WORKDIR /app
ENTRYPOINT ["python", "-m", "langflow", "run"] ENTRYPOINT ["python", "-m", "langflow", "run"]
CMD ["--host", "0.0.0.0", "--port", "7860"] CMD ["--host", "0.0.0.0", "--port", "7860"]

View file

@ -10,7 +10,7 @@
# PYTHON-BASE # PYTHON-BASE
# Sets up all our shared environment variables # Sets up all our shared environment variables
################################ ################################
FROM python:3.10-slim as python-base FROM python:3.12-slim as python-base
# python # python
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
@ -55,6 +55,8 @@ RUN apt-get update \
build-essential \ build-essential \
# npm # npm
npm \ npm \
# gcc
gcc \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@ -78,15 +80,11 @@ RUN cp -r src/frontend/build src/backend/base/langflow/frontend
RUN rm -rf src/backend/base/dist RUN rm -rf src/backend/base/dist
RUN cd src/backend/base && $POETRY_HOME/bin/poetry build --format sdist RUN cd src/backend/base && $POETRY_HOME/bin/poetry build --format sdist
# Final stage for the application
FROM python-base as final
# Copy virtual environment and built .tar.gz from builder base # Copy virtual environment and built .tar.gz from builder base
RUN useradd -m -u 1000 user RUN useradd -m -u 1000 user
COPY --from=builder-base /app/src/backend/base/dist/*.tar.gz ./
# Install the package from the .tar.gz # Install the package from the .tar.gz
RUN pip install *.tar.gz --user RUN python -m pip install /app/dist/*.tar.gz --user
WORKDIR /app
ENTRYPOINT ["python", "-m", "langflow", "run"] ENTRYPOINT ["python", "-m", "langflow", "run"]
CMD ["--host", "0.0.0.0", "--port", "7860"] CMD ["--host", "0.0.0.0", "--port", "7860"]

View file

@ -0,0 +1,45 @@
import ZoomableImage from "/src/theme/ZoomableImage.js";
import Admonition from "@theme/Admonition";
# Global environment variables
Langflow 1.0 alpha includes the option to add **Global Environment Variables** for your application.
## Add a global variable to a project
In this example, you'll add the `openai_api_key` credential as a global environment variable to the **Basic Prompting** starter project.
For more information on the starter flow, see [Basic prompting](../starter-projects/basic-prompting.mdx).
1. From the Langflow dashboard, click **New Project**.
2. Select **Basic Prompting**.
The **Basic Prompting** flow is created.
3. To create an environment variable for the **OpenAI** component:
1. In the **OpenAI API Key** field, click the **Globe** button, and then click **Add New Variable**.
2. In the **Variable Name** field, enter `openai_api_key`.
3. In the **Value** field, paste your OpenAI API Key (`sk-...`).
4. For the variable **Type**, select **Credential**.
5. In the **Apply to Fields** field, select **OpenAI API Key** to apply this variable to all fields named **OpenAI API Key**.
6. Click **Save Variable**.
You now have a `openai_api_key` global environment variable for your Langflow project.
<Admonition type="tip">
You can also create global variables in **Settings** > **Variables and
Secrets**.
</Admonition>
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/global-env.png",
dark: "img/global-env.png",
}}
style={{ width: "40%", margin: "20px auto" }}
/>
4. To view and manage your project's global environment variables, visit **Settings** > **Variables and Secrets**.
For more on variables in HuggingFace Spaces, see [Managing Secrets](https://huggingface.co/docs/hub/spaces-overview#managing-secrets).

View file

@ -0,0 +1,29 @@
import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
import ReactPlayer from "react-player";
import Admonition from "@theme/Admonition";
# Playground
In Langflow 1.0 alpha, the **Playground** replaces the **Interaction Panel**.
The **Playground** provides an interface for interacting with flows without opening them in the flow editor.
It even works for flows hosted on the Langflow store!
As long as you have a flow's environment variables set, you can run it by clicking the **Playground** button.
1. From your **Collections** page, click **Playground** in one of your flows.
The **Playground** window opens.
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: useBaseUrl("img/playground-chat.png"),
dark: useBaseUrl("img/playground-chat.png"),
}}
style={{ width: "50%", maxWidth: "600px", margin: "0 auto" }}
/>
2. Chat with your bot as you normally would, all without having to open the editor.

View file

@ -4,9 +4,7 @@ module.exports = {
type: "category", type: "category",
label: "What's New?", label: "What's New?",
collapsed: false, collapsed: false,
items: [ items: ["whats-new/a-new-chapter-langflow"],
"whats-new/a-new-chapter-langflow"
],
}, },
{ {
type: "category", type: "category",
@ -17,7 +15,7 @@ module.exports = {
"getting-started/install-langflow", "getting-started/install-langflow",
"getting-started/quickstart", "getting-started/quickstart",
"getting-started/huggingface-spaces", "getting-started/huggingface-spaces",
"getting-started/new-to-llms" "getting-started/new-to-llms",
], ],
}, },
{ {
@ -29,7 +27,7 @@ module.exports = {
"starter-projects/blog-writer", "starter-projects/blog-writer",
"starter-projects/document-qa", "starter-projects/document-qa",
"starter-projects/memory-chatbot", "starter-projects/memory-chatbot",
"starter-projects/vector-store-rag" "starter-projects/vector-store-rag",
], ],
}, },
{ {
@ -40,10 +38,12 @@ module.exports = {
"administration/login", "administration/login",
"administration/api", "administration/api",
"administration/cli", "administration/cli",
"administration/playground",
"administration/global-env",
"administration/components", "administration/components",
"administration/collection", "administration/collection",
"administration/prompt-customization", "administration/prompt-customization",
"administration/langfuse_integration" "administration/langfuse_integration",
], ],
}, },
{ {
@ -58,7 +58,7 @@ module.exports = {
"components/helpers", "components/helpers",
"components/vector-stores", "components/vector-stores",
"components/embeddings", "components/embeddings",
"components/custom" "components/custom",
], ],
}, },
{ {
@ -74,7 +74,7 @@ module.exports = {
"components/retrievers", "components/retrievers",
"components/text-splitters", "components/text-splitters",
"components/toolkits", "components/toolkits",
"components/tools" "components/tools",
], ],
}, },
{ {
@ -88,7 +88,7 @@ module.exports = {
"examples/csv-loader", "examples/csv-loader",
"examples/searchapi-tool", "examples/searchapi-tool",
"examples/serp-api-tool", "examples/serp-api-tool",
"examples/python-function" "examples/python-function",
], ],
}, },
{ {
@ -101,8 +101,8 @@ module.exports = {
"migration/inputs-and-outputs", "migration/inputs-and-outputs",
"migration/text-and-record", "migration/text-and-record",
"migration/compatibility", "migration/compatibility",
"migration/global-variables" "migration/global-variables",
] ],
}, },
{ {
type: "category", type: "category",
@ -111,16 +111,14 @@ module.exports = {
items: [ items: [
"tutorials/chatprompttemplate_guide", "tutorials/chatprompttemplate_guide",
"tutorials/loading_document", "tutorials/loading_document",
"tutorials/rag-with-astradb" "tutorials/rag-with-astradb",
], ],
}, },
{ {
type: "category", type: "category",
label: "Deployment", label: "Deployment",
collapsed: true, collapsed: true,
items: [ items: ["deployment/gcp-deployment"],
"deployment/gcp-deployment"
],
}, },
{ {
type: "category", type: "category",
@ -130,7 +128,7 @@ module.exports = {
"contributing/how-contribute", "contributing/how-contribute",
"contributing/github-issues", "contributing/github-issues",
"contributing/community", "contributing/community",
"contributing/contribute-component" "contributing/contribute-component",
], ],
}, },
], ],

BIN
docs/static/img/global-env.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
docs/static/img/playground-chat.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

750
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "1.0.0a33" version = "1.0.0a34"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Langflow <contact@langflow.org>"] authors = ["Langflow <contact@langflow.org>"]
maintainers = [ maintainers = [

View file

@ -1,65 +1,85 @@
import { Construct } from 'constructs'; import { Construct } from "constructs";
import * as ec2 from 'aws-cdk-lib/aws-ec2' import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds"; import * as rds from "aws-cdk-lib/aws-rds";
import * as cdk from 'aws-cdk-lib'; import * as cdk from "aws-cdk-lib";
interface RdsProps { interface RdsProps {
vpc: ec2.Vpc vpc: ec2.Vpc;
dbSG:ec2.SecurityGroup dbSG: ec2.SecurityGroup;
} }
export class Rds extends Construct{ export class Rds extends Construct {
readonly rdsCluster: rds.DatabaseCluster readonly rdsCluster: rds.DatabaseCluster;
constructor(scope: Construct, id:string, props: RdsProps){ constructor(scope: Construct, id: string, props: RdsProps) {
super(scope, id); super(scope, id);
const {vpc, dbSG} = props const { vpc, dbSG } = props;
const instanceType = ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MEDIUM) const instanceType = ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE4_GRAVITON,
ec2.InstanceSize.MEDIUM,
);
// RDSのパスワードを自動生成してSecrets Managerに格納 // RDSのパスワードを自動生成してSecrets Managerに格納
const rdsCredentials = rds.Credentials.fromGeneratedSecret('db_user',{ const rdsCredentials = rds.Credentials.fromGeneratedSecret("db_user", {
secretName: 'langflow-DbSecret', secretName: "langflow-DbSecret",
}) });
// DB クラスターのパラメータグループ作成 // DB クラスターのパラメータグループ作成
const clusterParameterGroup = new rds.ParameterGroup(scope, 'ClusterParameterGroup',{ const clusterParameterGroup = new rds.ParameterGroup(
engine: rds.DatabaseClusterEngine.auroraMysql({ scope,
version: rds.AuroraMysqlEngineVersion.VER_3_02_0 "ClusterParameterGroup",
}), {
description: 'for-langflow', engine: rds.DatabaseClusterEngine.auroraMysql({
}) version: rds.AuroraMysqlEngineVersion.of(
clusterParameterGroup.bindToCluster({}) "8.0.mysql_aurora.3.05.2",
"8.0",
),
}),
description: "for-langflow",
},
);
clusterParameterGroup.bindToCluster({});
// DB インスタンスのパラメタグループ作成 // DB インスタンスのパラメタグループ作成
const instanceParameterGroup = new rds.ParameterGroup(scope, 'InstanceParameterGroup',{ const instanceParameterGroup = new rds.ParameterGroup(
engine: rds.DatabaseClusterEngine.auroraMysql({ scope,
version: rds.AuroraMysqlEngineVersion.VER_3_02_0, "InstanceParameterGroup",
}), {
description: 'for-langflow', engine: rds.DatabaseClusterEngine.auroraMysql({
}) version: rds.AuroraMysqlEngineVersion.of(
instanceParameterGroup.bindToInstance({}) "8.0.mysql_aurora.3.05.2",
"8.0",
),
}),
description: "for-langflow",
},
);
instanceParameterGroup.bindToInstance({});
this.rdsCluster = new rds.DatabaseCluster(scope, 'LangflowDbCluster', { this.rdsCluster = new rds.DatabaseCluster(scope, "LangflowDbCluster", {
engine: rds.DatabaseClusterEngine.auroraMysql({ engine: rds.DatabaseClusterEngine.auroraMysql({
version: rds.AuroraMysqlEngineVersion.VER_3_02_0, version: rds.AuroraMysqlEngineVersion.of(
"8.0.mysql_aurora.3.05.2",
"8.0",
),
}), }),
storageEncrypted: true, storageEncrypted: true,
credentials: rdsCredentials, credentials: rdsCredentials,
instanceIdentifierBase: 'langflow-instance', instanceIdentifierBase: "langflow-instance",
vpc:vpc, vpc: vpc,
vpcSubnets:vpc.selectSubnets({ vpcSubnets: vpc.selectSubnets({
subnetGroupName: 'langflow-Isolated', subnetGroupName: "langflow-Isolated",
}), }),
securityGroups:[dbSG], securityGroups: [dbSG],
writer: rds.ClusterInstance.provisioned("WriterInstance", { writer: rds.ClusterInstance.provisioned("WriterInstance", {
instanceType: instanceType, instanceType: instanceType,
enablePerformanceInsights: true, enablePerformanceInsights: true,
parameterGroup:instanceParameterGroup, parameterGroup: instanceParameterGroup,
}), }),
// 2台目以降はreaders:で設定 // 2台目以降はreaders:で設定
parameterGroup: clusterParameterGroup, parameterGroup: clusterParameterGroup,
defaultDatabaseName: 'langflow', defaultDatabaseName: "langflow",
}) });
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -11,21 +11,21 @@
"cdk": "cdk" "cdk": "cdk"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.1", "@types/jest": "^29.5.12",
"@types/node": "20.1.7", "@types/node": "^20.12.12",
"aws-cdk": "^2.86.0", "aws-cdk": "^2.141.0",
"jest": "^29.5.0", "jest": "^29.7.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.2",
"typescript": "~5.1.3" "typescript": "^5.4.5"
}, },
"dependencies": { "dependencies": {
"@aws-solutions-constructs/aws-cloudfront-s3": "^2.49.0", "@aws-solutions-constructs/aws-cloudfront-s3": "^2.57.0",
"aws-cdk-lib": "^2.124.0", "aws-cdk-lib": "^2.141.0",
"cdk-ecr-deployment": "^2.5.30", "cdk-ecr-deployment": "^3.0.55",
"constructs": "^10.0.0", "constructs": "^10.3.0",
"deploy-time-build": "^0.3.12", "deploy-time-build": "^0.3.21",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
} }
} }

View file

@ -48,8 +48,10 @@ apt -y upgrade
# Install Python 3 pip, Langflow, and Nginx # Install Python 3 pip, Langflow, and Nginx
apt -y install python3-pip apt -y install python3-pip
pip install langflow pip3 install pip -U
langflow --host 0.0.0.0 --port 7860 apt -y update
pip3 install langflow
langflow run --host 0.0.0.0 --port 7860
EOF EOF
) )

View file

@ -131,3 +131,4 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
*.db

View file

@ -2,9 +2,9 @@ import platform
import socket import socket
import sys import sys
import time import time
import warnings
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import warnings
import click import click
import httpx import httpx
@ -443,6 +443,41 @@ def superuser(
typer.echo("Superuser creation failed.") typer.echo("Superuser creation failed.")
# command to copy the langflow database from the cache to the current directory
# because now the database is stored per installation
@app.command()
def copy_db():
"""
Copy the database files to the current directory.
This function copies the 'langflow.db' and 'langflow-pre.db' files from the cache directory to the current directory.
If the files exist in the cache directory, they will be copied to the same directory as this script (__main__.py).
Returns:
None
"""
import shutil
from platformdirs import user_cache_dir
cache_dir = Path(user_cache_dir("langflow"))
db_path = cache_dir / "langflow.db"
pre_db_path = cache_dir / "langflow-pre.db"
# It should be copied to the current directory
# this file is __main__.py and it should be in the same directory as the database
destination_folder = Path(__file__).parent
if db_path.exists():
shutil.copy(db_path, destination_folder)
typer.echo(f"Database copied to {destination_folder}")
else:
typer.echo("Database not found in the cache directory.")
if pre_db_path.exists():
shutil.copy(pre_db_path, destination_folder)
typer.echo(f"Pre-release database copied to {destination_folder}")
else:
typer.echo("Pre-release database not found in the cache directory.")
@app.command() @app.command()
def migration( def migration(
test: bool = typer.Option(True, help="Run migrations in test mode."), test: bool = typer.Option(True, help="Run migrations in test mode."),

View file

@ -49,7 +49,6 @@ class ChatComponent(CustomComponent):
sender: Optional[str] = None, sender: Optional[str] = None,
sender_name: Optional[str] = None, sender_name: Optional[str] = None,
) -> list[Record]: ) -> list[Record]:
records = store_message( records = store_message(
message, message,
session_id=session_id, session_id=session_id,

View file

@ -1,7 +1,5 @@
from typing import Optional from typing import Optional
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema.schema import Record from langflow.schema.schema import Record

View file

@ -1,9 +1,9 @@
from typing import List, Optional
from pydantic.v1 import SecretStr from pydantic.v1 import SecretStr
from langchain_mistralai.embeddings import MistralAIEmbeddings from langchain_mistralai.embeddings import MistralAIEmbeddings
from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.custom_component import CustomComponent
from langflow.field_typing import Embeddings, NestedDict from langflow.field_typing import Embeddings
class MistralAIEmbeddingsComponent(CustomComponent): class MistralAIEmbeddingsComponent(CustomComponent):
display_name = "MistralAI Embeddings" display_name = "MistralAI Embeddings"
@ -37,11 +37,7 @@ class MistralAIEmbeddingsComponent(CustomComponent):
"advanced": True, "advanced": True,
"value": 120, "value": 120,
}, },
"endpoint": { "endpoint": {"display_name": "API Endpoint", "advanced": True, "value": "https://api.mistral.ai/v1/"},
"display_name": "API Endpoint",
"advanced": True,
"value": "https://api.mistral.ai/v1/"
}
} }
def build( def build(
@ -51,7 +47,7 @@ class MistralAIEmbeddingsComponent(CustomComponent):
max_concurrent_requests: int = 64, max_concurrent_requests: int = 64,
max_retries: int = 5, max_retries: int = 5,
timeout: int = 120, timeout: int = 120,
endpoint: str = "https://api.mistral.ai/v1/" endpoint: str = "https://api.mistral.ai/v1/",
) -> Embeddings: ) -> Embeddings:
if mistral_api_key: if mistral_api_key:
api_key = SecretStr(mistral_api_key) api_key = SecretStr(mistral_api_key)
@ -64,6 +60,5 @@ class MistralAIEmbeddingsComponent(CustomComponent):
endpoint=endpoint, endpoint=endpoint,
max_concurrent_requests=max_concurrent_requests, max_concurrent_requests=max_concurrent_requests,
max_retries=max_retries, max_retries=max_retries,
timeout=timeout timeout=timeout,
) )

View file

@ -3,6 +3,7 @@ from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record from langflow.schema import Record
from langflow.field_typing import Text from langflow.field_typing import Text
class PassComponent(CustomComponent): class PassComponent(CustomComponent):
display_name = "Pass" display_name = "Pass"
description = "A pass-through component that forwards the second input while ignoring the first, used for controlling workflow direction." description = "A pass-through component that forwards the second input while ignoring the first, used for controlling workflow direction."
@ -19,7 +20,7 @@ class PassComponent(CustomComponent):
"display_name": "Input", "display_name": "Input",
"info": "This input is forwarded by the component.", "info": "This input is forwarded by the component.",
"input_types": ["Text", "Record"], "input_types": ["Text", "Record"],
} },
} }
def build(self, ignored_input: Text, forwarded_input: Text) -> Union[Text, Record]: def build(self, ignored_input: Text, forwarded_input: Text) -> Union[Text, Record]:

View file

@ -32,7 +32,6 @@ class StoreMessageComponent(CustomComponent):
session_id: Optional[str] = None, session_id: Optional[str] = None,
message: str = "", message: str = "",
) -> List[Record]: ) -> List[Record]:
store_message( store_message(
sender=sender, sender=sender,
sender_name=sender_name, sender_name=sender_name,

View file

@ -35,7 +35,7 @@ class SubFlowComponent(CustomComponent):
build_config["flow_name"]["options"] = self.get_flow_names() build_config["flow_name"]["options"] = self.get_flow_names()
# Clean up the build config # Clean up the build config
for key in list(build_config.keys()): for key in list(build_config.keys()):
if key not in self.field_order + ["code", "_type"]: if key not in self.field_order + ["code", "_type", "get_final_results_only"]:
del build_config[key] del build_config[key]
if field_value is not None and field_name == "flow_name": if field_value is not None and field_name == "flow_name":
try: try:
@ -85,20 +85,29 @@ class SubFlowComponent(CustomComponent):
"display_name": "Tweaks", "display_name": "Tweaks",
"info": "Tweaks to apply to the flow.", "info": "Tweaks to apply to the flow.",
}, },
"get_final_results_only": {
"display_name": "Get Final Results Only",
"info": "If False, the output will contain all outputs from the flow.",
"advanced": True,
},
} }
def build_records_from_result_data(self, result_data: ResultData) -> List[Record]: def build_records_from_result_data(self, result_data: ResultData, get_final_results_only: bool) -> List[Record]:
messages = result_data.messages messages = result_data.messages
if not messages: if not messages:
return [] return []
records = [] records = []
for message in messages: for message in messages:
message_dict = message if isinstance(message, dict) else message.model_dump() message_dict = message if isinstance(message, dict) else message.model_dump()
record = Record(data={"result": result_data.model_dump(), "message": message_dict.get("message", "")}) if get_final_results_only:
result_data_dict = result_data.model_dump()
results = result_data_dict.get("results", {})
inner_result = results.get("result", {})
record = Record(data={"result": inner_result, "message": message_dict}, text_key="result")
records.append(record) records.append(record)
return records return records
async def build(self, flow_name: str, **kwargs) -> List[Record]: async def build(self, flow_name: str, get_final_results_only: bool = True, **kwargs) -> List[Record]:
tweaks = {key: {"input_value": value} for key, value in kwargs.items()} tweaks = {key: {"input_value": value} for key, value in kwargs.items()}
run_outputs: List[Optional[RunOutputs]] = await self.run_flow( run_outputs: List[Optional[RunOutputs]] = await self.run_flow(
tweaks=tweaks, tweaks=tweaks,
@ -112,7 +121,7 @@ class SubFlowComponent(CustomComponent):
if run_output is not None: if run_output is not None:
for output in run_output.outputs: for output in run_output.outputs:
if output: if output:
records.extend(self.build_records_from_result_data(output)) records.extend(self.build_records_from_result_data(output, get_final_results_only))
self.status = records self.status = records
logger.debug(records) logger.debug(records)

View file

@ -4,6 +4,7 @@ from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record from langflow.schema import Record
from langflow.field_typing import Text from langflow.field_typing import Text
class TextOperatorComponent(CustomComponent): class TextOperatorComponent(CustomComponent):
display_name = "Text Operator" display_name = "Text Operator"
description = "Compares two text inputs based on a specified condition such as equality or inequality, with optional case sensitivity." description = "Compares two text inputs based on a specified condition such as equality or inequality, with optional case sensitivity."
@ -21,14 +22,7 @@ class TextOperatorComponent(CustomComponent):
"operator": { "operator": {
"display_name": "Operator", "display_name": "Operator",
"info": "The operator to apply for comparing the texts.", "info": "The operator to apply for comparing the texts.",
"options": [ "options": ["equals", "not equals", "contains", "starts with", "ends with", "exists"],
"equals",
"not equals",
"contains",
"starts with",
"ends with",
"exists"
],
}, },
"case_sensitive": { "case_sensitive": {
"display_name": "Case Sensitive", "display_name": "Case Sensitive",
@ -51,11 +45,8 @@ class TextOperatorComponent(CustomComponent):
case_sensitive: bool = False, case_sensitive: bool = False,
true_output: Optional[Text] = "", true_output: Optional[Text] = "",
) -> Union[Text, Record]: ) -> Union[Text, Record]:
if not input_text or not match_text: if not input_text or not match_text:
raise ValueError( raise ValueError("Both 'input_text' and 'match_text' must be provided and non-empty.")
"Both 'input_text' and 'match_text' must be provided and non-empty."
)
if not case_sensitive: if not case_sensitive:
input_text = input_text.lower() input_text = input_text.lower()

View file

@ -242,6 +242,7 @@ class Graph:
outputs: list[str], outputs: list[str],
stream: bool, stream: bool,
session_id: str, session_id: str,
fallback_to_env_vars: bool,
) -> List[Optional["ResultData"]]: ) -> List[Optional["ResultData"]]:
""" """
Runs the graph with the given inputs. Runs the graph with the given inputs.
@ -289,7 +290,7 @@ class Graph:
start_component_id = next( start_component_id = next(
(vertex_id for vertex_id in self._is_input_vertices if "chat" in vertex_id.lower()), None (vertex_id for vertex_id in self._is_input_vertices if "chat" in vertex_id.lower()), None
) )
await self.process(start_component_id=start_component_id) await self.process(start_component_id=start_component_id, fallback_to_env_vars=fallback_to_env_vars)
self.increment_run_count() self.increment_run_count()
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.exception(exc)
@ -315,6 +316,7 @@ class Graph:
outputs: Optional[list[str]] = None, outputs: Optional[list[str]] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
stream: bool = False, stream: bool = False,
fallback_to_env_vars: bool = False,
) -> List[RunOutputs]: ) -> List[RunOutputs]:
""" """
Run the graph with the given inputs and return the outputs. Run the graph with the given inputs and return the outputs.
@ -340,6 +342,7 @@ class Graph:
outputs=outputs, outputs=outputs,
session_id=session_id, session_id=session_id,
stream=stream, stream=stream,
fallback_to_env_vars=fallback_to_env_vars,
) )
try: try:
@ -362,6 +365,7 @@ class Graph:
outputs: Optional[list[str]] = None, outputs: Optional[list[str]] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
stream: bool = False, stream: bool = False,
fallback_to_env_vars: bool = False,
) -> List[RunOutputs]: ) -> List[RunOutputs]:
""" """
Runs the graph with the given inputs. Runs the graph with the given inputs.
@ -403,6 +407,7 @@ class Graph:
outputs=outputs or [], outputs=outputs or [],
stream=stream, stream=stream,
session_id=session_id or "", session_id=session_id or "",
fallback_to_env_vars=fallback_to_env_vars,
) )
run_output_object = RunOutputs(inputs=run_inputs, outputs=run_outputs) run_output_object = RunOutputs(inputs=run_inputs, outputs=run_outputs)
logger.debug(f"Run outputs: {run_output_object}") logger.debug(f"Run outputs: {run_output_object}")
@ -468,9 +473,9 @@ class Graph:
"""Marks a branch of the graph.""" """Marks a branch of the graph."""
if visited is None: if visited is None:
visited = set() visited = set()
visited.add(vertex_id)
if vertex_id in visited: if vertex_id in visited:
return return
visited.add(vertex_id)
self.mark_vertex(vertex_id, state) self.mark_vertex(vertex_id, state)
@ -712,6 +717,7 @@ class Graph:
vertex_id: str, vertex_id: str,
inputs_dict: Optional[Dict[str, str]] = None, inputs_dict: Optional[Dict[str, str]] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
fallback_to_env_vars: bool = False,
): ):
""" """
Builds a vertex in the graph. Builds a vertex in the graph.
@ -733,7 +739,7 @@ class Graph:
vertex = self.get_vertex(vertex_id) vertex = self.get_vertex(vertex_id)
try: try:
if not vertex.frozen or not vertex._built: if not vertex.frozen or not vertex._built:
await vertex.build(user_id=user_id, inputs=inputs_dict) await vertex.build(user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars)
if vertex.result is not None: if vertex.result is not None:
params = vertex._built_object_repr() params = vertex._built_object_repr()
@ -796,7 +802,7 @@ class Graph:
vertices.append(vertex) vertices.append(vertex)
return vertices return vertices
async def process(self, start_component_id: Optional[str] = None) -> "Graph": async def process(self, fallback_to_env_vars: bool, start_component_id: Optional[str] = None) -> "Graph":
"""Processes the graph with vertices in each layer run in parallel.""" """Processes the graph with vertices in each layer run in parallel."""
first_layer = self.sort_vertices(start_component_id=start_component_id) first_layer = self.sort_vertices(start_component_id=start_component_id)
@ -821,6 +827,7 @@ class Graph:
vertex_id=vertex_id, vertex_id=vertex_id,
user_id=self.user_id, user_id=self.user_id,
inputs_dict={}, inputs_dict={},
fallback_to_env_vars=fallback_to_env_vars,
), ),
name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}", name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}",
) )

View file

@ -315,7 +315,11 @@ class Vertex:
params[field_name] = full_path params[field_name] = full_path
elif field.get("required"): elif field.get("required"):
field_display_name = field.get("display_name") field_display_name = field.get("display_name")
raise ValueError(f"File path not found for {field_display_name} in component {self.display_name}") logger.warning(
f"File path not found for {field_display_name} in component {self.display_name}. Setting to None."
)
params[field_name] = None
elif field.get("type") in DIRECT_TYPES and params.get(field_name) is None: elif field.get("type") in DIRECT_TYPES and params.get(field_name) is None:
val = field.get("value") val = field.get("value")
if field.get("type") == "code": if field.get("type") == "code":
@ -390,13 +394,17 @@ class Vertex:
self.params = self._raw_params.copy() self.params = self._raw_params.copy()
self.updated_raw_params = True self.updated_raw_params = True
async def _build(self, user_id=None): async def _build(
self,
fallback_to_env_vars,
user_id=None,
):
""" """
Initiate the build process. Initiate the build process.
""" """
logger.debug(f"Building {self.display_name}") logger.debug(f"Building {self.display_name}")
await self._build_each_vertex_in_params_dict(user_id) await self._build_each_vertex_in_params_dict(user_id)
await self._get_and_instantiate_class(user_id) await self._get_and_instantiate_class(user_id, fallback_to_env_vars)
self._validate_built_object() self._validate_built_object()
self._built = True self._built = True
@ -606,7 +614,7 @@ class Vertex:
if isinstance(self.params[key], list): if isinstance(self.params[key], list):
self.params[key].extend(result) self.params[key].extend(result)
async def _get_and_instantiate_class(self, user_id=None): async def _get_and_instantiate_class(self, user_id=None, fallback_to_env_vars=False):
""" """
Gets the class from a dictionary and instantiates it with the params. Gets the class from a dictionary and instantiates it with the params.
""" """
@ -615,6 +623,7 @@ class Vertex:
try: try:
result = await loading.instantiate_class( result = await loading.instantiate_class(
user_id=user_id, user_id=user_id,
fallback_to_env_vars=fallback_to_env_vars,
vertex=self, vertex=self,
) )
self._update_built_object_and_artifacts(result) self._update_built_object_and_artifacts(result)

View file

@ -1,8 +1,8 @@
import inspect import inspect
import json import json
import os
from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type
import orjson import orjson
from langchain.agents import agent as agent_module from langchain.agents import agent as agent_module
from langchain.agents.agent import AgentExecutor from langchain.agents.agent import AgentExecutor
@ -35,6 +35,7 @@ if TYPE_CHECKING:
async def instantiate_class( async def instantiate_class(
vertex: "Vertex", vertex: "Vertex",
fallback_to_env_vars,
user_id=None, user_id=None,
) -> Any: ) -> Any:
"""Instantiate class from module type and key, and params""" """Instantiate class from module type and key, and params"""
@ -57,7 +58,7 @@ async def instantiate_class(
if not base_type: if not base_type:
raise ValueError("No base type provided for vertex") raise ValueError("No base type provided for vertex")
if base_type == "custom_components": if base_type == "custom_components":
return await instantiate_custom_component(params, user_id, vertex) return await instantiate_custom_component(params, user_id, vertex, fallback_to_env_vars=fallback_to_env_vars)
class_object = import_by_type(_type=base_type, name=vertex_type) class_object = import_by_type(_type=base_type, name=vertex_type)
return await instantiate_based_on_type( return await instantiate_based_on_type(
class_object=class_object, class_object=class_object,
@ -66,6 +67,7 @@ async def instantiate_class(
params=params, params=params,
user_id=user_id, user_id=user_id,
vertex=vertex, vertex=vertex,
fallback_to_env_vars=fallback_to_env_vars,
) )
@ -93,14 +95,7 @@ def convert_kwargs(params):
return params return params
async def instantiate_based_on_type( async def instantiate_based_on_type(class_object, base_type, node_type, params, user_id, vertex, fallback_to_env_vars):
class_object,
base_type,
node_type,
params,
user_id,
vertex,
):
if base_type == "agents": if base_type == "agents":
return instantiate_agent(node_type, class_object, params) return instantiate_agent(node_type, class_object, params)
elif base_type == "prompts": elif base_type == "prompts":
@ -132,33 +127,49 @@ async def instantiate_based_on_type(
elif base_type == "memory": elif base_type == "memory":
return instantiate_memory(node_type, class_object, params) return instantiate_memory(node_type, class_object, params)
elif base_type == "custom_components": elif base_type == "custom_components":
return await instantiate_custom_component( return await instantiate_custom_component(params, user_id, vertex, fallback_to_env_vars=fallback_to_env_vars)
params,
user_id,
vertex,
)
elif base_type == "wrappers": elif base_type == "wrappers":
return instantiate_wrapper(node_type, class_object, params) return instantiate_wrapper(node_type, class_object, params)
else: else:
return class_object(**params) return class_object(**params)
def update_params_with_load_from_db_fields(custom_component: "CustomComponent", params, load_from_db_fields): def update_params_with_load_from_db_fields(
custom_component: "CustomComponent", params, load_from_db_fields, fallback_to_env_vars=False
):
# For each field in load_from_db_fields, we will check if it's in the params # For each field in load_from_db_fields, we will check if it's in the params
# and if it is, we will get the value from the custom_component.keys(name) # and if it is, we will get the value from the custom_component.keys(name)
# and update the params with the value # and update the params with the value
for field in load_from_db_fields: for field in load_from_db_fields:
if field in params: if field in params:
try: try:
key = custom_component.variables(params[field]) key = None
params[field] = key if key else params[field] try:
key = custom_component.variables(params[field])
except ValueError as e:
# check if "User id is not set" is in the error message
if "User id is not set" in str(e) and not fallback_to_env_vars:
raise e
logger.debug(str(e))
if fallback_to_env_vars and key is None:
var = os.getenv(params[field])
if var is None:
raise ValueError(f"Environment variable {params[field]} is not set.")
key = var
logger.info(f"Using environment variable {params[field]} for {field}")
if key is None:
logger.warning(f"Could not get value for {field}. Setting it to None.")
params[field] = key
except Exception as exc: except Exception as exc:
logger.error(f"Failed to get value for {field} from custom component. Error: {exc}") logger.error(f"Failed to get value for {field} from custom component. Setting it to None. Error: {exc}")
pass
params[field] = None
return params return params
async def instantiate_custom_component(params, user_id, vertex): async def instantiate_custom_component(params, user_id, vertex, fallback_to_env_vars: bool = False):
params_copy = params.copy() params_copy = params.copy()
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code")) class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
custom_component: "CustomComponent" = class_object( custom_component: "CustomComponent" = class_object(
@ -167,7 +178,9 @@ async def instantiate_custom_component(params, user_id, vertex):
vertex=vertex, vertex=vertex,
selected_output_type=vertex.selected_output_type, selected_output_type=vertex.selected_output_type,
) )
params_copy = update_params_with_load_from_db_fields(custom_component, params_copy, vertex.load_from_db_fields) params_copy = update_params_with_load_from_db_fields(
custom_component, params_copy, vertex.load_from_db_fields, fallback_to_env_vars
)
if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"): if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"):
params_copy["retriever"] = params_copy["retriever"].as_retriever() params_copy["retriever"] = params_copy["retriever"].as_retriever()

View file

@ -5,7 +5,6 @@ import orjson
from langchain_community.vectorstores import ( from langchain_community.vectorstores import (
FAISS, FAISS,
Chroma, Chroma,
ElasticsearchStore,
MongoDBAtlasVectorSearch, MongoDBAtlasVectorSearch,
Pinecone, Pinecone,
Qdrant, Qdrant,

View file

@ -108,7 +108,6 @@ def store_message(
sender: Optional[str] = None, sender: Optional[str] = None,
sender_name: Optional[str] = None, sender_name: Optional[str] = None,
) -> list[Record]: ) -> list[Record]:
if not message: if not message:
warnings.warn("No message provided.") warnings.warn("No message provided.")
return [] return []

View file

@ -82,6 +82,7 @@ def run_flow_from_json(
env_file: Optional[str] = None, env_file: Optional[str] = None,
cache: Optional[str] = None, cache: Optional[str] = None,
disable_logs: Optional[bool] = True, disable_logs: Optional[bool] = True,
fallback_to_env_vars: bool = False,
) -> List[RunOutputs]: ) -> List[RunOutputs]:
""" """
Run a flow from a JSON file or dictionary. Run a flow from a JSON file or dictionary.
@ -98,6 +99,7 @@ def run_flow_from_json(
env_file (Optional[str], optional): The environment file to load. Defaults to None. env_file (Optional[str], optional): The environment file to load. Defaults to None.
cache (Optional[str], optional): The cache directory to use. Defaults to None. cache (Optional[str], optional): The cache directory to use. Defaults to None.
disable_logs (Optional[bool], optional): Whether to disable logs. Defaults to True. disable_logs (Optional[bool], optional): Whether to disable logs. Defaults to True.
fallback_to_env_vars (bool, optional): Whether Global Variables should fallback to environment variables if not found. Defaults to False.
Returns: Returns:
List[RunOutputs]: A list of RunOutputs objects representing the results of running the flow. List[RunOutputs]: A list of RunOutputs objects representing the results of running the flow.
@ -127,5 +129,6 @@ def run_flow_from_json(
input_type=input_type, input_type=input_type,
output_type=output_type, output_type=output_type,
output_component=output_component, output_component=output_component,
fallback_to_env_vars=fallback_to_env_vars,
) )
return result return result

View file

@ -175,6 +175,7 @@ def run_graph(
input_value: str, input_value: str,
input_type: str, input_type: str,
output_type: str, output_type: str,
fallback_to_env_vars: bool = False,
output_component: Optional[str] = None, output_component: Optional[str] = None,
) -> List[RunOutputs]: ) -> List[RunOutputs]:
""" """
@ -218,6 +219,7 @@ def run_graph(
outputs or [], outputs or [],
stream=False, stream=False,
session_id="", session_id="",
fallback_to_env_vars=fallback_to_env_vars,
) )
return run_outputs return run_outputs

View file

@ -4,7 +4,6 @@ from typing import Literal, Optional, cast
from langchain_core.documents import Document from langchain_core.documents import Document
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from pydantic import BaseModel, model_validator from pydantic import BaseModel, model_validator
from langchain_core.messages import HumanMessage, AIMessage
class Record(BaseModel): class Record(BaseModel):

View file

@ -7,13 +7,12 @@ from typing import Any, List, Optional, Tuple, Type
import orjson import orjson
import yaml import yaml
from langflow.services.settings.constants import VARIABLES_TO_GET_FROM_ENVIRONMENT
from loguru import logger from loguru import logger
from pydantic import field_validator, validator from pydantic import field_validator
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict
from langflow.services.settings.constants import VARIABLES_TO_GET_FROM_ENVIRONMENT
# BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components") # BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components")
BASE_COMPONENTS_PATH = str(Path(__file__).parent.parent.parent / "components") BASE_COMPONENTS_PATH = str(Path(__file__).parent.parent.parent / "components")
@ -76,6 +75,10 @@ class Settings(BaseSettings):
# Define the default LANGFLOW_DIR # Define the default LANGFLOW_DIR
CONFIG_DIR: Optional[str] = None CONFIG_DIR: Optional[str] = None
# Define if langflow db should be saved in config dir or
# in the langflow directory
SAVE_DB_IN_CONFIG_DIR: bool = False
"""Define if langflow database should be saved in LANGFLOW_CONFIG_DIR or in the langflow directory (i.e. in the package directory)."""
DEV: bool = False DEV: bool = False
DATABASE_URL: Optional[str] = None DATABASE_URL: Optional[str] = None
@ -108,12 +111,16 @@ class Settings(BaseSettings):
CELERY_ENABLED: bool = False CELERY_ENABLED: bool = False
fallback_to_env_var: bool = True
"""If set to True, Global Variables set in the UI will fallback to a environment variable
with the same name in case Langflow fails to retrieve the variable value."""
store_environment_variables: bool = True store_environment_variables: bool = True
"""Whether to store environment variables as Global Variables in the database.""" """Whether to store environment variables as Global Variables in the database."""
variables_to_get_from_environment: list[str] = VARIABLES_TO_GET_FROM_ENVIRONMENT variables_to_get_from_environment: list[str] = VARIABLES_TO_GET_FROM_ENVIRONMENT
"""List of environment variables to get from the environment and store in the database.""" """List of environment variables to get from the environment and store in the database."""
@validator("CONFIG_DIR", pre=True, allow_reuse=True) @field_validator("CONFIG_DIR", mode="before")
def set_langflow_dir(cls, value): def set_langflow_dir(cls, value):
if not value: if not value:
from platformdirs import user_cache_dir from platformdirs import user_cache_dir
@ -136,8 +143,8 @@ class Settings(BaseSettings):
return str(value) return str(value)
@validator("DATABASE_URL", pre=True) @field_validator("DATABASE_URL", mode="before")
def set_database_url(cls, value, values): def set_database_url(cls, value, info):
if not value: if not value:
logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable") logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable")
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
@ -148,29 +155,36 @@ class Settings(BaseSettings):
# Originally, we used sqlite:///./langflow.db # Originally, we used sqlite:///./langflow.db
# so we need to migrate to the new format # so we need to migrate to the new format
# if there is a database in that location # if there is a database in that location
if not values["CONFIG_DIR"]: if not info.data["CONFIG_DIR"]:
raise ValueError("CONFIG_DIR not set, please set it or provide a DATABASE_URL") raise ValueError("CONFIG_DIR not set, please set it or provide a DATABASE_URL")
from langflow.version import is_pre_release # type: ignore from langflow.version import is_pre_release # type: ignore
if info.data["SAVE_DB_IN_CONFIG_DIR"]:
database_dir = info.data["CONFIG_DIR"]
logger.debug(f"Saving database to CONFIG_DIR: {database_dir}")
else:
database_dir = Path(__file__).parent.parent.parent.resolve()
logger.debug(f"Saving database to langflow directory: {database_dir}")
pre_db_file_name = "langflow-pre.db" pre_db_file_name = "langflow-pre.db"
db_file_name = "langflow.db" db_file_name = "langflow.db"
new_pre_path = f"{values['CONFIG_DIR']}/{pre_db_file_name}" new_pre_path = f"{database_dir}/{pre_db_file_name}"
new_path = f"{values['CONFIG_DIR']}/{db_file_name}" new_path = f"{database_dir}/{db_file_name}"
final_path = None final_path = None
if is_pre_release: if is_pre_release:
if Path(new_pre_path).exists(): if Path(new_pre_path).exists():
final_path = new_pre_path final_path = new_pre_path
elif Path(new_path).exists(): elif Path(new_path).exists() and info.data["SAVE_DB_IN_CONFIG_DIR"]:
# We need to copy the current db to the new location # We need to copy the current db to the new location
logger.debug("Copying existing database to new location") logger.debug("Copying existing database to new location")
copy2(new_path, new_pre_path) copy2(new_path, new_pre_path)
logger.debug(f"Copied existing database to {new_pre_path}") logger.debug(f"Copied existing database to {new_pre_path}")
elif Path(f"./{db_file_name}").exists(): elif Path(f"./{db_file_name}").exists() and info.data["SAVE_DB_IN_CONFIG_DIR"]:
logger.debug("Copying existing database to new location") logger.debug("Copying existing database to new location")
copy2(f"./{db_file_name}", new_pre_path) copy2(f"./{db_file_name}", new_pre_path)
logger.debug(f"Copied existing database to {new_pre_path}") logger.debug(f"Copied existing database to {new_pre_path}")
else: else:
logger.debug(f"Database already exists at {new_pre_path}, using it") logger.debug(f"Creating new database at {new_pre_path}")
final_path = new_pre_path final_path = new_pre_path
else: else:
if Path(new_path).exists(): if Path(new_path).exists():
@ -311,3 +325,6 @@ def load_settings_from_yaml(file_path: str) -> Settings:
logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}")
return Settings(**settings_dict) return Settings(**settings_dict)
return Settings(**settings_dict)
return Settings(**settings_dict)
return Settings(**settings_dict)

View file

@ -1,11 +1,10 @@
import os import os
import yaml import yaml
from loguru import logger
from langflow.services.base import Service from langflow.services.base import Service
from langflow.services.settings.auth import AuthSettings from langflow.services.settings.auth import AuthSettings
from langflow.services.settings.base import Settings from langflow.services.settings.base import Settings
from loguru import logger
class SettingsService(Service): class SettingsService(Service):
@ -31,7 +30,7 @@ class SettingsService(Service):
for key in settings_dict: for key in settings_dict:
if key not in Settings.model_fields.keys(): if key not in Settings.model_fields.keys():
raise KeyError(f"Key {key} not found in settings") logger.warning(f"Key {key} not found in settings")
logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}")
settings = Settings(**settings_dict) settings = Settings(**settings_dict)

View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -1137,20 +1137,20 @@ extended-testing = ["faker (>=19.3.1,<20.0.0)", "jinja2 (>=3,<4)", "pandas (>=2.
[[package]] [[package]]
name = "langchain-text-splitters" name = "langchain-text-splitters"
version = "0.0.1" version = "0.0.2"
description = "LangChain text splitting utilities" description = "LangChain text splitting utilities"
optional = false optional = false
python-versions = ">=3.8.1,<4.0" python-versions = "<4.0,>=3.8.1"
files = [ files = [
{file = "langchain_text_splitters-0.0.1-py3-none-any.whl", hash = "sha256:f5b802f873f5ff6a8b9259ff34d53ed989666ef4e1582e6d1adb3b5520e3839a"}, {file = "langchain_text_splitters-0.0.2-py3-none-any.whl", hash = "sha256:13887f32705862c1e1454213cb7834a63aae57c26fcd80346703a1d09c46168d"},
{file = "langchain_text_splitters-0.0.1.tar.gz", hash = "sha256:ac459fa98799f5117ad5425a9330b21961321e30bc19a2a2f9f761ddadd62aa1"}, {file = "langchain_text_splitters-0.0.2.tar.gz", hash = "sha256:ac8927dc0ba08eba702f6961c9ed7df7cead8de19a9f7101ab2b5ea34201b3c1"},
] ]
[package.dependencies] [package.dependencies]
langchain-core = ">=0.1.28,<0.2.0" langchain-core = ">=0.1.28,<0.3"
[package.extras] [package.extras]
extended-testing = ["lxml (>=5.1.0,<6.0.0)"] extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"]
[[package]] [[package]]
name = "langchainhub" name = "langchainhub"
@ -1169,13 +1169,13 @@ types-requests = ">=2.31.0.2,<3.0.0.0"
[[package]] [[package]]
name = "langsmith" name = "langsmith"
version = "0.1.57" version = "0.1.59"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false optional = false
python-versions = "<4.0,>=3.8.1" python-versions = "<4.0,>=3.8.1"
files = [ files = [
{file = "langsmith-0.1.57-py3-none-any.whl", hash = "sha256:dbd83b0944a2fbea4151f0aa053530d93fcf6784a580621bc60633cb890b57dc"}, {file = "langsmith-0.1.59-py3-none-any.whl", hash = "sha256:445e3bc1d3baa1e5340cd979907a19483b9763a2ed37b863a01113d406f69345"},
{file = "langsmith-0.1.57.tar.gz", hash = "sha256:4682204de19f0218029c2b8445ce2cc3485c8d0df9796b31e2ce4c9051fce365"}, {file = "langsmith-0.1.59.tar.gz", hash = "sha256:e748a89f4dd6aa441349143e49e546c03b5dfb43376a25bfef6a5ca792fe1437"},
] ]
[package.dependencies] [package.dependencies]
@ -1282,9 +1282,13 @@ files = [
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
@ -1935,13 +1939,13 @@ xmp = ["defusedxml"]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.2.1" version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
] ]
[package.extras] [package.extras]
@ -2180,13 +2184,13 @@ cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "python-engineio" name = "python-engineio"
version = "4.9.0" version = "4.9.1"
description = "Engine.IO server and client for Python" description = "Engine.IO server and client for Python"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "python-engineio-4.9.0.tar.gz", hash = "sha256:e87459c15638e567711fd156e6f9c4a402668871bed79523f0ecfec744729ec7"}, {file = "python_engineio-4.9.1-py3-none-any.whl", hash = "sha256:f995e702b21f6b9ebde4e2000cd2ad0112ba0e5116ec8d22fe3515e76ba9dddd"},
{file = "python_engineio-4.9.0-py3-none-any.whl", hash = "sha256:979859bff770725b75e60353d7ae53b397e8b517d05ba76733b404a3dcca3e4c"}, {file = "python_engineio-4.9.1.tar.gz", hash = "sha256:7631cf5563086076611e494c643b3fa93dd3a854634b5488be0bba0ef9b99709"},
] ]
[package.dependencies] [package.dependencies]

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow-base" name = "langflow-base"
version = "0.0.44" version = "0.0.45"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Langflow <contact@langflow.org>"] authors = ["Langflow <contact@langflow.org>"]
maintainers = [ maintainers = [

View file

@ -1,141 +1,162 @@
import { saveAs } from "file-saver";
import OpenSeadragon from "openseadragon";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import ForwardedIconComponent from "../genericIconComponent";
import useFlowStore from "../../stores/flowStore";
import OpenSeadragon from 'openseadragon';
import { Separator } from "../ui/separator";
import { saveAs } from 'file-saver'
import useAlertStore from "../../stores/alertStore";
import { IMGViewErrorMSG, IMGViewErrorTitle } from "../../constants/constants"; import { IMGViewErrorMSG, IMGViewErrorTitle } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import ForwardedIconComponent from "../genericIconComponent";
import { Separator } from "../ui/separator";
export default function ImageViewer({image }) { export default function ImageViewer({ image }) {
const viewerRef = useRef(null); const viewerRef = useRef(null);
const [errorDownloading, setErrordownloading] = useState(false) const [errorDownloading, setErrordownloading] = useState(false);
const setErrorList = useAlertStore(state => state.setErrorData); const setErrorList = useAlertStore((state) => state.setErrorData);
const [initialMsg, setInicialMsg] = useState("Please build your flow"); const [initialMsg, setInicialMsg] = useState("Please build your flow");
useEffect(() => {
try {
if (viewerRef.current) {
// Initialize OpenSeadragon viewer
const viewer = OpenSeadragon({
element: viewerRef.current,
prefixUrl:
"https://cdnjs.cloudflare.com/ajax/libs/openseadragon/2.4.2/images/", // Optional: Set the path to OpenSeadragon images
tileSources: { type: "image", url: image },
defaultZoomLevel: 1,
maxZoomPixelRatio: 4,
showNavigationControl: false,
});
const zoomInButton = document.getElementById("zoom-in-button");
const zoomOutButton = document.getElementById("zoom-out-button");
const homeButton = document.getElementById("home-button");
const fullPageButton = document.getElementById("full-page-button");
useEffect(() => { zoomInButton!.addEventListener("click", () =>
try { viewer.viewport.zoomBy(1.2),
if (viewerRef.current) { );
// Initialize OpenSeadragon viewer zoomOutButton!.addEventListener("click", () =>
const viewer = OpenSeadragon({ viewer.viewport.zoomBy(0.8),
element: viewerRef.current, );
prefixUrl: 'https://cdnjs.cloudflare.com/ajax/libs/openseadragon/2.4.2/images/', // Optional: Set the path to OpenSeadragon images homeButton!.addEventListener("click", () => viewer.viewport.goHome());
tileSources: {type: 'image', url: image}, fullPageButton!.addEventListener("click", () =>
defaultZoomLevel: 1, viewer.setFullScreen(true),
maxZoomPixelRatio: 4, );
showNavigationControl: false,
});
const zoomInButton = document.getElementById('zoom-in-button');
const zoomOutButton = document.getElementById('zoom-out-button');
const homeButton = document.getElementById('home-button');
const fullPageButton = document.getElementById('full-page-button');
zoomInButton!.addEventListener('click', () => viewer.viewport.zoomBy(1.2)); // Optionally, you can set additional viewer options here
zoomOutButton!.addEventListener('click', () => viewer.viewport.zoomBy(0.8));
homeButton!.addEventListener('click', () => viewer.viewport.goHome());
fullPageButton!.addEventListener('click', () => viewer.setFullScreen(true));
// Optionally, you can set additional viewer options here // Cleanup function
return () => {
// Cleanup function viewer.destroy();
return () => { zoomInButton!.removeEventListener("click", () =>
viewer.destroy(); viewer.viewport.zoomBy(1.2),
zoomInButton!.removeEventListener('click', () => viewer.viewport.zoomBy(1.2)); );
zoomOutButton!.removeEventListener('click', () => viewer.viewport.zoomBy(0.8)); zoomOutButton!.removeEventListener("click", () =>
homeButton!.removeEventListener('click', () => viewer.viewport.goHome()); viewer.viewport.zoomBy(0.8),
fullPageButton!.removeEventListener('click', () => viewer.setFullScreen(true)); );
}; homeButton!.removeEventListener("click", () =>
} viewer.viewport.goHome(),
} catch (error) { );
console.error('Error initializing OpenSeadragon:', error); fullPageButton!.removeEventListener("click", () =>
} viewer.setFullScreen(true),
}, [image]); );
};
function download() {
const imageUrl = image;
// Fetch the image data
fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
// Save the image using FileSaver.js
saveAs(blob, 'image.jpg');
})
.catch(error => {
setErrorList({title: "There was an error downloading your image"})
console.error('Error downloading image:', error)
});
} }
} catch (error) {
console.error("Error initializing OpenSeadragon:", error);
}
}, [image]);
return ( function download() {
image === "" ? ( const imageUrl = image;
<div className="w-full h-full bg-muted rounded-md flex align-center justify-center flex-col gap-5 border border-border"> // Fetch the image data
<div className="flex gap-2 align-center justify-center "> fetch(imageUrl)
<ForwardedIconComponent .then((response) => response.blob())
name="Image" .then((blob) => {
/> // Save the image using FileSaver.js
{IMGViewErrorTitle} saveAs(blob, "image.jpg");
</div> })
<div className="flex align-center justify-center"> .catch((error) => {
<div className="langflow-chat-desc flex align-center justify-center"> setErrorList({ title: "There was an error downloading your image" });
<div className="langflow-chat-desc-span"> console.error("Error downloading image:", error);
{IMGViewErrorMSG} });
</div> }
</div>
</div> return image === "" ? (
<div className="align-center flex h-full w-full flex-col justify-center gap-5 rounded-md border border-border bg-muted">
<div className="align-center flex justify-center gap-2 ">
<ForwardedIconComponent name="Image" />
{IMGViewErrorTitle}
</div>
<div className="align-center flex justify-center">
<div className="langflow-chat-desc align-center flex justify-center">
<div className="langflow-chat-desc-span">{IMGViewErrorMSG}</div>
</div>
</div>
</div>
) : (
<>
<div className="align-center my-2 mb-4 flex w-full justify-center">
<div className="shadow-round-btn-shadow hover:shadow-round-btn-shadow flex w-[50%] items-center justify-center rounded-sm border bg-muted shadow-md transition-all">
<button
id="zoom-in-button"
className="relative inline-flex w-full w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="ZoomIn"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div> </div>
) : ( <button
<> id="zoom-out-button"
<div className="w-full flex align-center justify-center my-2 mb-4"> className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
<div className="shadow-round-btn-shadow hover:shadow-round-btn-shadow flex items-center justify-center rounded-sm border bg-muted shadow-md transition-all w-[50%]"> >
<button id="zoom-in-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all w-full transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"> <ForwardedIconComponent
<ForwardedIconComponent name="ZoomOut"
name="ZoomIn" className={"h-5 w-5 text-secondary-foreground"}
className={"text-secondary-foreground w-5 h-5"} />
/> </button>
</button> <div>
<div> <Separator orientation="vertical" />
<Separator orientation="vertical" />
</div>
<button id="zoom-out-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="ZoomOut"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button id="home-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="RotateCcw"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button id="full-page-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="Maximize2"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button onClick={download} className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="ArrowDownToLine"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
</div>
</div> </div>
<div id="canvas" ref={viewerRef} className={`w-full h-[90%] `} /> <button
</> id="home-button"
) className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
); >
<ForwardedIconComponent
name="RotateCcw"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button
id="full-page-button"
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="Maximize2"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button
onClick={download}
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="ArrowDownToLine"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
</div>
</div>
<div id="canvas" ref={viewerRef} className={`h-[90%] w-full `} />
</>
);
} }

View file

@ -6,6 +6,7 @@ import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore"; import { useStoreStore } from "../../stores/storeStore";
import { FlowType } from "../../types/flow";
import { storeComponent } from "../../types/store"; import { storeComponent } from "../../types/store";
import cloneFLowWithParent, { import cloneFLowWithParent, {
getInputsAndOutputs, getInputsAndOutputs,
@ -22,7 +23,6 @@ import {
CardTitle, CardTitle,
} from "../ui/card"; } from "../ui/card";
import Loading from "../ui/loading"; import Loading from "../ui/loading";
import { FlowType } from "../../types/flow";
export default function CollectionCardComponent({ export default function CollectionCardComponent({
data, data,

View file

@ -26,6 +26,8 @@ import {
TabsTrigger, TabsTrigger,
} from "../../components/ui/tabs"; } from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants"; import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import ExportModal from "../../modals/exportModal";
import { Case } from "../../shared/components/caseComponent";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import { codeTabsPropsType } from "../../types/components"; import { codeTabsPropsType } from "../../types/components";
@ -43,7 +45,6 @@ import KeypairListComponent from "../keypairListComponent";
import ShadTooltip from "../shadTooltipComponent"; import ShadTooltip from "../shadTooltipComponent";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
import ExportModal from "../../modals/exportModal";
export default function CodeTabsComponent({ export default function CodeTabsComponent({
flow, flow,
@ -54,6 +55,7 @@ export default function CodeTabsComponent({
tweaks, tweaks,
setActiveTweaks, setActiveTweaks,
activeTweaks, activeTweaks,
allowExport = false,
}: codeTabsPropsType) { }: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false); const [isCopied, setIsCopied] = useState<Boolean>(false);
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null); const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
@ -87,6 +89,10 @@ export default function CodeTabsComponent({
}); });
}; };
const type = (node, templateParam) => {
return node.data.node.template[templateParam].type;
};
const downloadAsFile = () => { const downloadAsFile = () => {
const fileExtension = tabs[activeTab].language || ".txt"; const fileExtension = tabs[activeTab].language || ".txt";
const suggestedFileName = `${"generated-code."}${fileExtension}`; const suggestedFileName = `${"generated-code."}${fileExtension}`;
@ -141,39 +147,43 @@ export default function CodeTabsComponent({
)} )}
<div className="float-right mx-1 mb-1 mt-2 flex gap-2"> <div className="float-right mx-1 mb-1 mt-2 flex gap-2">
<div {tweaks && (
className={ <div
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
>
<Switch
style={{
transform: `scaleX(${0.7}) scaleY(${0.7})`,
}}
id="tweaks-switch"
onCheckedChange={setActiveTweaks}
autoFocus={false}
/>
<Label
className={ className={
"relative right-1 top-[4px] text-xs font-medium text-gray-500 dark:text-gray-300 " + Number(activeTab) > 2
(activeTweaks ? "hidden"
? "font-bold text-black dark:text-white" : "relative top-[2.5px] flex gap-2"
: "font-medium")
} }
htmlFor="tweaks-switch"
> >
Tweaks <Switch
</Label> style={{
</div> transform: `scaleX(${0.7}) scaleY(${0.7})`,
<ExportModal> }}
<div className="flex cursor-pointer items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"> id="tweaks-switch"
<IconComponent name="FileDown" className="h-4 w-4" /> onCheckedChange={setActiveTweaks}
Export Flow autoFocus={false}
/>
<Label
className={
"relative right-1 top-[4px] text-xs font-medium text-gray-500 dark:text-gray-300 " +
(activeTweaks
? "font-bold text-black dark:text-white"
: "font-medium")
}
htmlFor="tweaks-switch"
>
Tweaks
</Label>
</div> </div>
</ExportModal> )}
{allowExport && (
<ExportModal>
<div className="flex cursor-pointer items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300">
<IconComponent name="FileDown" className="h-4 w-4" />
Export Flow
</div>
</ExportModal>
)}
{Number(activeTab) < 4 && ( {Number(activeTab) < 4 && (
<> <>
@ -276,107 +286,71 @@ export default function CodeTabsComponent({
</TableCell> </TableCell>
<TableCell className="p-0 text-xs text-foreground"> <TableCell className="p-0 text-xs text-foreground">
<div className="m-auto w-[250px]"> <div className="m-auto w-[250px]">
{node.data.node.template[ <Case
templateField condition={
].type === "str" && type(node, templateField) ===
!node.data.node.template[ "str" &&
templateField !node.data.node.template[
].options ? (
<div className="mx-auto">
{node.data.node.template[
templateField templateField
]?.list ? ( ].options
<InputListComponent }
componentName={ >
templateField <Case
} condition={
editNode={true} node.data.node.template[
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? [""]
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
],
);
}}
/>
) : node.data.node.template[
templateField templateField
].multiline ? ( ]?.list
<div> }
<TextAreaComponent >
disabled={false} <InputListComponent
editNode={true} componentName={templateField}
value={ editNode={true}
!node.data.node disabled={false}
.template[ value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? [""]
: node.data.node.template[
templateField templateField
].value || ].value
node.data.node.template[ }
templateField onChange={(target) => {
].value === "" setData((old) => {
? "" let newInputList =
: node.data.node cloneDeep(old);
.template[ newInputList![
templateField i
].value ].data.node.template[
} templateField
onChange={(target) => { ].value = target;
setData((old) => { return newInputList;
let newInputList = });
cloneDeep(old); tweaks?.buildTweakObject!(
newInputList![ node["data"]["id"],
i target,
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node
.template[
templateField
],
);
}}
/>
</div>
) : (
<InputComponent
editNode={true}
disabled={false}
password={
node.data.node.template[ node.data.node.template[
templateField templateField
].password ?? false ],
} );
}}
/>
</Case>
<Case
condition={
node.data.node.template[
templateField
].multiline
}
>
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={ value={
!node.data.node.template[ !node.data.node.template[
templateField templateField
@ -410,11 +384,68 @@ export default function CodeTabsComponent({
); );
}} }}
/> />
)} </div>
</div> </Case>
) : node.data.node.template[
templateField <Case
].type === "bool" ? ( condition={
!node.data.node.template[
templateField
].multiline &&
!node.data.node.template[
templateField
].list
}
>
<InputComponent
editNode={true}
disabled={false}
password={
node.data.node.template[
templateField
].password ?? false
}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
],
);
}}
/>
</Case>
</Case>
<Case
condition={
type(node, templateField) ===
"bool"
}
>
<div className="ml-auto"> <div className="ml-auto">
{" "} {" "}
<ToggleShadComponent <ToggleShadComponent
@ -446,9 +477,14 @@ export default function CodeTabsComponent({
disabled={false} disabled={false}
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "file" ? ( <Case
condition={
type(node, templateField) ===
"file"
}
>
<div className="mx-auto"> <div className="mx-auto">
<InputFileComponent <InputFileComponent
editNode={true} editNode={true}
@ -473,9 +509,14 @@ export default function CodeTabsComponent({
}} }}
></InputFileComponent> ></InputFileComponent>
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "float" ? ( <Case
condition={
type(node, templateField) ===
"float"
}
>
<div className="mx-auto"> <div className="mx-auto">
<FloatComponent <FloatComponent
disabled={false} disabled={false}
@ -518,12 +559,17 @@ export default function CodeTabsComponent({
}} }}
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "str" && <Case
node.data.node.template[ condition={
templateField type(node, templateField) ===
].options ? ( "str" &&
node.data.node.template[
templateField
].options
}
>
<div className="mx-auto"> <div className="mx-auto">
<Dropdown <Dropdown
editNode={true} editNode={true}
@ -565,9 +611,14 @@ export default function CodeTabsComponent({
} }
></Dropdown> ></Dropdown>
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "int" ? ( <Case
condition={
type(node, templateField) ===
"int"
}
>
<div className="mx-auto"> <div className="mx-auto">
<IntComponent <IntComponent
disabled={false} disabled={false}
@ -610,9 +661,14 @@ export default function CodeTabsComponent({
}} }}
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "prompt" ? ( <Case
condition={
type(node, templateField) ===
"prompt"
}
>
<div className="mx-auto"> <div className="mx-auto">
<PromptAreaComponent <PromptAreaComponent
readonly={true} readonly={true}
@ -651,9 +707,14 @@ export default function CodeTabsComponent({
}} }}
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "code" ? ( <Case
condition={
type(node, templateField) ===
"code"
}
>
<div className="mx-auto"> <div className="mx-auto">
<CodeAreaComponent <CodeAreaComponent
disabled={false} disabled={false}
@ -692,9 +753,14 @@ export default function CodeTabsComponent({
}} }}
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "dict" ? ( <Case
condition={
type(node, templateField) ===
"dict"
}
>
<div className="mx-auto overflow-auto custom-scroll"> <div className="mx-auto overflow-auto custom-scroll">
<KeypairListComponent <KeypairListComponent
disabled={false} disabled={false}
@ -712,6 +778,10 @@ export default function CodeTabsComponent({
.template[ .template[
templateField templateField
].value, ].value,
type(
node,
templateField,
),
) )
} }
duplicateKey={ duplicateKey={
@ -755,9 +825,14 @@ export default function CodeTabsComponent({
} }
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "NestedDict" ? ( <Case
condition={
type(node, templateField) ===
"NestedDict"
}
>
<div className="mx-auto"> <div className="mx-auto">
<DictComponent <DictComponent
disabled={false} disabled={false}
@ -765,7 +840,7 @@ export default function CodeTabsComponent({
value={ value={
node.data.node!.template[ node.data.node!.template[
templateField templateField
].value.toString() === "{}" ].value?.toString() === "{}"
? { ? {
// yourkey: "value", // yourkey: "value",
} }
@ -795,13 +870,16 @@ export default function CodeTabsComponent({
}} }}
/> />
</div> </div>
) : node.data.node.template[ </Case>
templateField
].type === "Any" ? ( <Case
"-" condition={
) : ( type(node, templateField) ===
<div className="hidden"></div> "Any"
)} }
>
<>-</>
</Case>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View file

@ -2,7 +2,6 @@ export const convertCSVToData = (csvFile, csvSeparator: string) => {
const lines = csvFile.data.trim().split("\n"); const lines = csvFile.data.trim().split("\n");
const headers = lines[0].trim().split(csvSeparator); const headers = lines[0].trim().split(csvSeparator);
const initialRowData: any = []; const initialRowData: any = [];
const initialColDefs = headers.map((header) => ({ const initialColDefs = headers.map((header) => ({
field: header.trim(), field: header.trim(),

View file

@ -59,7 +59,7 @@ export default function Dropdown({
? "dropdown-component-outline" ? "dropdown-component-outline"
: "dropdown-component-false-outline", : "dropdown-component-false-outline",
"w-full justify-between font-normal", "w-full justify-between font-normal",
editNode ? "input-edit-node" : "py-2" editNode ? "input-edit-node" : "py-2",
)} )}
> >
<span data-testid={`value-dropdown-` + id}> <span data-testid={`value-dropdown-` + id}>
@ -78,6 +78,8 @@ export default function Dropdown({
</PopoverTrigger> </PopoverTrigger>
)} )}
<PopoverContentDropdown <PopoverContentDropdown
side="bottom"
avoidCollisions={!!children}
className="nocopy nowheel nopan nodelete nodrag noundo p-0" className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={ style={
children children
@ -105,7 +107,7 @@ export default function Dropdown({
name="Check" name="Check"
className={cn( className={cn(
"ml-auto h-4 w-4 text-primary", "ml-auto h-4 w-4 text-primary",
value === option ? "opacity-100" : "opacity-0" value === option ? "opacity-100" : "opacity-0",
)} )}
/> />
</CommandItem> </CommandItem>

View file

@ -169,6 +169,7 @@ export default function InputComponent({
className="nocopy nopan nodelete nodrag noundo p-0" className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }} style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom" side="bottom"
avoidCollisions={false}
align="center" align="center"
> >
<Command <Command

View file

@ -1,23 +1,19 @@
import { CHAT_FIRST_INITIAL_TEXT, CHAT_SECOND_INITIAL_TEXT, PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants"; import { PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants";
import IconComponent from "../../genericIconComponent"; import IconComponent from "../../genericIconComponent";
export default function Error(): JSX.Element { export default function Error(): JSX.Element {
return ( return (
<div className="flex flex-col items-center justify-center h-full w-full bg-muted"> <div className="flex h-full w-full flex-col items-center justify-center bg-muted">
<div className="chat-alert-box"> <div className="chat-alert-box">
<span className="flex gap-2"> <span className="flex gap-2">
<IconComponent name="FileX2" /> <IconComponent name="FileX2" />
<span className="langflow-chat-span">{PDFLoadErrorTitle}</span> <span className="langflow-chat-span">{PDFLoadErrorTitle}</span>
</span> </span>
<br /> <br />
<div className="langflow-chat-desc"> <div className="langflow-chat-desc">
<span className="langflow-chat-desc-span"> <span className="langflow-chat-desc-span">{PDFCheckFlow} </span>
{PDFCheckFlow}{" "}
</span>
</div>
</div>
</div> </div>
); </div>
</div>
);
} }

View file

@ -1,5 +1,5 @@
import { AxiosRequestConfig, AxiosResponse } from "axios"; import { AxiosRequestConfig, AxiosResponse } from "axios";
import { Edge, ReactFlowJsonObject,Node } from "reactflow"; import { Edge, Node, ReactFlowJsonObject } from "reactflow";
import { BASE_URL_API } from "../../constants/constants"; import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api"; import { api } from "../../controllers/API/api";
import { import {
@ -59,7 +59,7 @@ export async function sendAll(data: sendAllProps) {
} }
export async function postValidateCode( export async function postValidateCode(
code: string code: string,
): Promise<AxiosResponse<errorsTypeAPI>> { ): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code }); return await api.post(`${BASE_URL_API}validate/code`, { code });
} }
@ -74,7 +74,7 @@ export async function postValidateCode(
export async function postValidatePrompt( export async function postValidatePrompt(
name: string, name: string,
template: string, template: string,
frontend_node: APIClassType frontend_node: APIClassType,
): Promise<AxiosResponse<PromptTypeAPI>> { ): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, { return api.post(`${BASE_URL_API}validate/prompt`, {
name, name,
@ -145,7 +145,7 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails. * @throws Will throw an error if the update fails.
*/ */
export async function updateFlowInDatabase( export async function updateFlowInDatabase(
updatedFlow: FlowType updatedFlow: FlowType,
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, { const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
@ -321,7 +321,7 @@ export async function getHealth() {
* *
*/ */
export async function getBuildStatus( export async function getBuildStatus(
flowId: string flowId: string,
): Promise<AxiosResponse<BuildStatusTypeAPI>> { ): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`); return await api.get(`${BASE_URL_API}build/${flowId}/status`);
} }
@ -334,7 +334,7 @@ export async function getBuildStatus(
* *
*/ */
export async function postBuildInit( export async function postBuildInit(
flow: FlowType flow: FlowType,
): Promise<AxiosResponse<InitTypeAPI>> { ): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow); return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
} }
@ -350,7 +350,7 @@ export async function postBuildInit(
*/ */
export async function uploadFile( export async function uploadFile(
file: File, file: File,
id: string id: string,
): Promise<AxiosResponse<UploadFileTypeAPI>> { ): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
@ -359,7 +359,7 @@ export async function uploadFile(
export async function postCustomComponent( export async function postCustomComponent(
code: string, code: string,
apiClass: APIClassType apiClass: APIClassType,
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template; // let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, { return await api.post(`${BASE_URL_API}custom_component`, {
@ -372,7 +372,7 @@ export async function postCustomComponentUpdate(
code: string, code: string,
template: APITemplateType, template: APITemplateType,
field: string, field: string,
field_value: any field_value: any,
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, { return await api.post(`${BASE_URL_API}custom_component/update`, {
code, code,
@ -394,7 +394,7 @@ export async function onLogin(user: LoginType) {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
} },
); );
if (response.status === 200) { if (response.status === 200) {
@ -456,11 +456,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage( export async function getUsersPage(
skip: number, skip: number,
limit: number limit: number,
): Promise<Array<Users>> { ): Promise<Array<Users>> {
try { try {
const res = await api.get( const res = await api.get(
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}` `${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
); );
if (res.status === 200) { if (res.status === 200) {
return res.data; return res.data;
@ -497,7 +497,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try { try {
const res = await api.patch( const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`, `${BASE_URL_API}users/${user_id}/reset-password`,
user user,
); );
if (res.status === 200) { if (res.status === 200) {
return res.data; return res.data;
@ -571,7 +571,7 @@ export async function saveFlowStore(
last_tested_version?: string; last_tested_version?: string;
}, },
tags: string[], tags: string[],
publicFlow = false publicFlow = false,
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await api.post(`${BASE_URL_API}store/components/`, { const response = await api.post(`${BASE_URL_API}store/components/`, {
@ -700,7 +700,7 @@ export async function postStoreComponents(component: Component) {
export async function getComponent(component_id: string) { export async function getComponent(component_id: string) {
try { try {
const res = await api.get( const res = await api.get(
`${BASE_URL_API}store/components/${component_id}` `${BASE_URL_API}store/components/${component_id}`,
); );
if (res.status === 200) { if (res.status === 200) {
return res.data; return res.data;
@ -715,7 +715,7 @@ export async function searchComponent(
page?: number | null, page?: number | null,
limit?: number | null, limit?: number | null,
status?: string | null, status?: string | null,
tags?: string[] tags?: string[],
): Promise<StoreComponentResponse | undefined> { ): Promise<StoreComponentResponse | undefined> {
try { try {
let url = `${BASE_URL_API}store/components/`; let url = `${BASE_URL_API}store/components/`;
@ -827,7 +827,7 @@ export async function updateFlowStore(
}, },
tags: string[], tags: string[],
publicFlow = false, publicFlow = false,
id: string id: string,
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, { const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
@ -911,7 +911,7 @@ export async function deleteGlobalVariable(id: string) {
export async function updateGlobalVariable( export async function updateGlobalVariable(
name: string, name: string,
value: string, value: string,
id: string id: string,
) { ) {
try { try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, { const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -929,36 +929,40 @@ export async function getVerticesOrder(
flowId: string, flowId: string,
startNodeId?: string | null, startNodeId?: string | null,
stopNodeId?: string | null, stopNodeId?: string | null,
nodes?:Node[], nodes?: Node[],
Edges?:Edge[] Edges?: Edge[],
): Promise<AxiosResponse<VerticesOrderTypeAPI>> { ): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter // nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices // if nodeId is not provided, the API will return all vertices
const config:AxiosRequestConfig<any> = {}; const config: AxiosRequestConfig<any> = {};
if (stopNodeId) { if (stopNodeId) {
config["params"] = { stop_component_id: stopNodeId }; config["params"] = { stop_component_id: stopNodeId };
} else if (startNodeId) { } else if (startNodeId) {
config["params"] = { start_component_id: startNodeId }; config["params"] = { start_component_id: startNodeId };
} }
const data = { const data = {
data:{} data: {},
};
if (nodes && Edges) {
data["data"]["nodes"] = nodes;
data["data"]["edges"] = Edges;
} }
if(nodes && Edges){ return await api.post(
data["data"]["nodes"] = nodes `${BASE_URL_API}build/${flowId}/vertices`,
data["data"]["edges"] = Edges data,
} config,
return await api.post(`${BASE_URL_API}build/${flowId}/vertices`,data, config); );
} }
export async function postBuildVertex( export async function postBuildVertex(
flowId: string, flowId: string,
vertexId: string, vertexId: string,
input_value: string input_value: string,
): Promise<AxiosResponse<VertexBuildTypeAPI>> { ): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter // input_value is optional and is a query parameter
return await api.post( return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`, `${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined input_value ? { inputs: { input_value: input_value } } : undefined,
); );
} }
@ -982,7 +986,7 @@ export async function getFlowPool({
} }
export async function deleteFlowPool( export async function deleteFlowPool(
flowId: string flowId: string,
): Promise<AxiosResponse<any>> { ): Promise<AxiosResponse<any>> {
const config = {}; const config = {};
config["params"] = { flow_id: flowId }; config["params"] = { flow_id: flowId };

View file

@ -1,5 +1,5 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import React, { ReactNode, useEffect, useRef, useState } from "react"; import { ReactNode, useEffect, useRef, useState } from "react";
import { Handle, Position, useUpdateNodeInternals } from "reactflow"; import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import CodeAreaComponent from "../../../../components/codeAreaComponent"; import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent"; import DictComponent from "../../../../components/dictComponent";
@ -18,20 +18,15 @@ import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton"; import { RefreshButton } from "../../../../components/ui/refreshButton";
import { import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES, LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY, TOOLTIP_EMPTY,
} from "../../../../constants/constants"; } from "../../../../constants/constants";
import { Case } from "../../../../shared/components/caseComponent";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore"; import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useTypesStore } from "../../../../stores/typesStore"; import { useTypesStore } from "../../../../stores/typesStore";
import { import { APIClassType } from "../../../../types/api";
APIClassType,
ResponseErrorDetailAPI,
ResponseErrorTypeAPI,
} from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components"; import { ParameterComponentType } from "../../../../types/components";
import { import {
debouncedHandleUpdateValues, debouncedHandleUpdateValues,
@ -44,13 +39,13 @@ import {
isValidConnection, isValidConnection,
scapedJSONStringfy, scapedJSONStringfy,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { import { nodeColors } from "../../../../utils/styleUtils";
nodeColors, import { classNames, groupByFamily } from "../../../../utils/utils";
nodeIconsLucide, import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
nodeNames, import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
} from "../../../../utils/styleUtils"; import useHandleNodeClass from "../../../hooks/use-handle-node-class";
import { classNames } from "../../../../utils/utils"; import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
import groupByFamily from "./utils/group-by-family"; import TooltipRenderComponent from "../tooltipRenderComponent";
export default function ParameterComponent({ export default function ParameterComponent({
left, left,
@ -76,175 +71,69 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges); const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode); const setNode = useFlowStore((state) => state.setNode);
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const updateNodeInternals = useUpdateNodeInternals();
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const flow = currentFlow?.data?.nodes ?? null; const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null); const groupedEdge = useRef(null);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
);
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
);
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
useHandleRefreshButtonPress(setIsLoading, setNode, renderTooltips);
let disabled = let disabled =
edges.some( edges.some(
(edge) => (edge) =>
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id), edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id),
) ?? false; ) ?? false;
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const handleRefreshButtonPress = async (name, data) => { const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true); handleRefreshButtonPressHook(name, data);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}; };
useEffect(() => { useFetchDataOnMount(
async function fetchData() { data,
if ( name,
(data.node?.template[name]?.real_time_refresh || handleUpdateValues,
data.node?.template[name]?.refresh_button) && setNode,
// options can be undefined but not an empty array renderTooltips,
(data.node?.template[name]?.options?.length ?? 0) === 0 setIsLoading,
) { );
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []);
const handleOnNewValue = async ( const handleOnNewValue = async (
newValue: string | string[] | boolean | Object[], newValue: string | string[] | boolean | Object[],
skipSnapshot: boolean | undefined = false, skipSnapshot: boolean | undefined = false,
): Promise<void> => { ): Promise<void> => {
const nodeTemplate = data.node!.template[name]; handleOnNewValueHook(newValue, skipSnapshot);
const currentValue = nodeTemplate.value;
if (currentValue !== newValue && !skipSnapshot) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
currentValue !== newValue;
const typeToDebounce = nodeTemplate.type;
nodeTemplate.value = newValue;
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
if (["int"].includes(typeToDebounce)) {
newTemplate = await handleUpdateValues(name, data);
} else {
newTemplate = await debouncedHandleUpdateValues(name, data);
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
}
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else {
newNode.data.node.template[name].value = newValue;
}
return newNode;
});
renderTooltips();
}; };
const updateNodeInternals = useUpdateNodeInternals();
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => { const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (!data.node) return; handleNodeClassHook(newNodeClass, code);
if (data.node!.template[name].value !== code) {
takeSnapshot();
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
newNode.data.node.template[name].value = code;
return newNode;
});
updateNodeInternals(data.id);
renderTooltips();
}; };
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => { useEffect(() => {
// @ts-ignore // @ts-ignore
infoHtml.current = ( infoHtml.current = (
@ -265,88 +154,7 @@ export default function ParameterComponent({
if (groupedObj && groupedObj.length > 0) { if (groupedObj && groupedObj.length > 0) {
//@ts-ignore //@ts-ignore
refHtml.current = groupedObj.map((item, index) => { refHtml.current = groupedObj.map((item, index) => {
const Icon: any = return <TooltipRenderComponent index={index} item={item} left={left} />;
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<div
key={index}
data-testid={`available-${left ? "input" : "output"}-${
item.family
}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span
className="ps-2 text-xs text-foreground"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span
className="text-xs"
data-testid={`tooltip-${item?.type}`}
>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
}); });
} else { } else {
//@ts-ignore //@ts-ignore
@ -355,6 +163,7 @@ export default function ParameterComponent({
); );
} }
} }
// If optionalHandle is an empty list, then it is not an optional handle // If optionalHandle is an empty list, then it is not an optional handle
if (optionalHandle && optionalHandle.length === 0) { if (optionalHandle && optionalHandle.length === 0) {
optionalHandle = null; optionalHandle = null;
@ -363,6 +172,9 @@ export default function ParameterComponent({
useEffect(() => { useEffect(() => {
renderTooltips(); renderTooltips();
}, [tooltipTitle, flow]); }, [tooltipTitle, flow]);
console.log(left === true && type === "dict");
return !showNode ? ( return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? ( left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></> <></>
@ -428,11 +240,12 @@ export default function ParameterComponent({
(left ? "" : " justify-end") (left ? "" : " justify-end")
} }
> >
{!left && data.node?.frozen && ( <Case condition={left && data.node?.frozen}>
<div className="pr-1"> <div className="pr-1">
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} /> <IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div> </div>
)} </Case>
{proxy ? ( {proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}> <ShadTooltip content={<span>{proxy.id}</span>}>
<span className={!left && data.node?.frozen ? " text-ice" : ""}> <span className={!left && data.node?.frozen ? " text-ice" : ""}>
@ -478,45 +291,37 @@ export default function ParameterComponent({
}`} }`}
type={left ? "target" : "source"} type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right} position={left ? Position.Left : Position.Right}
key={ key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
proxy id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) => isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges) isValidConnection(connection, nodes, edges)
} }
className={classNames( className={classNames(
left ? "-ml-0.5 " : "-mr-0.5 ", left ? "-ml-0.5" : "-mr-0.5",
"h-3 w-3 rounded-full border-2 bg-background", "h-3 w-3 rounded-full border-2 bg-background",
)} )}
style={{ style={{ borderColor: color ?? nodeColors.unknown }}
borderColor: color ?? nodeColors.unknown, onClick={() => setFilterEdge(groupedEdge.current)}
}} />
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip> </ShadTooltip>
</div> </div>
</Button> </Button>
)} )}
{left === true && <Case
type === "str" && condition={
!data.node?.template[name].options ? ( left === true &&
type === "str" &&
!data.node?.template[name]?.options
}
>
<div className="w-full"> <div className="w-full">
{data.node?.template[name].list ? ( <Case condition={data.node?.template[name]?.list}>
<div <div
className={ className={
// Commenting this out until we have a better // Commenting this out until we have a better
// way to display // way to display
// (data.node?.template[name].refresh ? "w-5/6 " : "") + // (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
"flex-grow" "flex-grow"
} }
> >
@ -524,39 +329,27 @@ export default function ParameterComponent({
componentName={name} componentName={name}
disabled={disabled} disabled={disabled}
value={ value={
!data.node.template[name].value || !data.node!.template[name]?.value ||
data.node.template[name].value === "" data.node!.template[name]?.value === ""
? [""] ? [""]
: data.node.template[name].value : data.node!.template[name]?.value
} }
onChange={handleOnNewValue} onChange={handleOnNewValue}
/> />
{/* {data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)} */}
</div> </div>
) : data.node?.template[name].multiline ? ( </Case>
<Case condition={data.node?.template[name]?.multiline}>
<div className="mt-2 flex w-full flex-col "> <div className="mt-2 flex w-full flex-col ">
<div className="flex-grow"> <div className="flex-grow">
<TextAreaComponent <TextAreaComponent
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node!.template[name]?.value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name} id={"textarea-" + data.node!.template[name]?.name}
data-testid={"textarea-" + data.node.template[name].name} data-testid={"textarea-" + data.node!.template[name]?.name}
/> />
</div> </div>
{data.node?.template[name].refresh_button && ( {data.node?.template[name]?.refresh_button && (
<div className="flex-grow"> <div className="flex-grow">
<RefreshButton <RefreshButton
isLoading={isLoading} isLoading={isLoading}
@ -564,7 +357,7 @@ export default function ParameterComponent({
name={name} name={name}
data={data} data={data}
button_text={ button_text={
data.node?.template[name].refresh_button_text ?? data.node?.template[name]?.refresh_button_text ??
"Refresh" "Refresh"
} }
className="extra-side-bar-buttons mt-1" className="extra-side-bar-buttons mt-1"
@ -574,12 +367,18 @@ export default function ParameterComponent({
</div> </div>
)} )}
</div> </div>
) : ( </Case>
<Case
condition={
!data.node?.template[name]?.multiline &&
!data.node?.template[name]?.list
}
>
<div className="mt-2 flex w-full items-center"> <div className="mt-2 flex w-full items-center">
<div <div
className={ className={
"flex-grow " + "flex-grow " +
(data.node?.template[name].refresh_button ? "w-5/6" : "") (data.node?.template[name]?.refresh_button ? "w-5/6" : "")
} }
> >
<InputGlobalComponent <InputGlobalComponent
@ -599,7 +398,7 @@ export default function ParameterComponent({
data={data} data={data}
/> />
</div> </div>
{data.node?.template[name].refresh_button && ( {data.node?.template[name]?.refresh_button && (
<div className="w-1/6"> <div className="w-1/6">
<RefreshButton <RefreshButton
isLoading={isLoading} isLoading={isLoading}
@ -607,7 +406,7 @@ export default function ParameterComponent({
name={name} name={name}
data={data} data={data}
button_text={ button_text={
data.node?.template[name].refresh_button_text ?? data.node?.template[name]?.refresh_button_text ??
"Refresh" "Refresh"
} }
className="extra-side-bar-buttons ml-2 mt-1" className="extra-side-bar-buttons ml-2 mt-1"
@ -617,52 +416,61 @@ export default function ParameterComponent({
</div> </div>
)} )}
</div> </div>
)} </Case>
</div> </div>
) : left === true && type === "bool" ? ( </Case>
<Case condition={left === true && type === "bool"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<ToggleShadComponent <ToggleShadComponent
id={"toggle-" + name} id={"toggle-" + name}
disabled={disabled} disabled={disabled}
enabled={data.node?.template[name].value ?? false} enabled={data.node?.template[name]?.value ?? false}
setEnabled={handleOnNewValue} setEnabled={handleOnNewValue}
size="large" size="large"
editNode={false} editNode={false}
/> />
</div> </div>
) : left === true && type === "float" ? ( </Case>
<Case condition={left === true && type === "float"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<FloatComponent <FloatComponent
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name]?.value ?? ""}
rangeSpec={data.node?.template[name]?.rangeSpec} rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue} onChange={handleOnNewValue}
/> />
</div> </div>
) : left === true && </Case>
type === "str" &&
(data.node?.template[name].options || <Case
data.node?.template[name]?.real_time_refresh) ? ( condition={
// TODO: Improve CSS left === true &&
type === "str" &&
(data.node?.template[name]?.options ||
data.node?.template[name]?.real_time_refresh)
}
>
<div className="mt-2 flex w-full items-center"> <div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow"> <div className="w-5/6 flex-grow">
<Dropdown <Dropdown
disabled={disabled} disabled={disabled}
isLoading={isLoading} isLoading={isLoading}
options={data.node.template[name].options} options={data.node!.template[name]?.options}
onSelect={handleOnNewValue} onSelect={handleOnNewValue}
value={data.node.template[name].value} value={data.node!.template[name]?.value}
id={"dropdown-" + name} id={"dropdown-" + name}
/> />
</div> </div>
{data.node?.template[name].refresh_button && ( {data.node?.template[name]?.refresh_button && (
<div className="w-1/6"> <div className="w-1/6">
<RefreshButton <RefreshButton
isLoading={isLoading} isLoading={isLoading}
disabled={disabled} disabled={disabled}
name={name} name={name}
data={data} data={data}
button_text={data.node?.template[name].refresh_button_text} button_text={data.node?.template[name]?.refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1" className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress} handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name} id={"refresh-button-" + name}
@ -670,46 +478,54 @@ export default function ParameterComponent({
</div> </div>
)} )}
</div> </div>
) : left === true && type === "code" ? ( </Case>
<Case condition={left === true && type === "code"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<CodeAreaComponent <CodeAreaComponent
readonly={ readonly={
data.node?.flow && data.node.template[name].dynamic data.node?.flow && data.node.template[name]?.dynamic
? true ? true
: false : false
} }
dynamic={data.node?.template[name].dynamic ?? false} dynamic={data.node?.template[name]?.dynamic ?? false}
setNodeClass={handleNodeClass} setNodeClass={handleNodeClass}
nodeClass={data.node} nodeClass={data.node}
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"code-input-" + name} id={"code-input-" + name}
/> />
</div> </div>
) : left === true && type === "file" ? ( </Case>
<Case condition={left === true && type === "file"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<InputFileComponent <InputFileComponent
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes} fileTypes={data.node?.template[name]?.fileTypes}
onFileChange={(filePath: string) => { onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath; data.node!.template[name].file_path = filePath;
}} }}
></InputFileComponent> ></InputFileComponent>
</div> </div>
) : left === true && type === "int" ? ( </Case>
<Case condition={left === true && type === "int"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<IntComponent <IntComponent
rangeSpec={data.node?.template[name].rangeSpec} rangeSpec={data.node?.template[name]?.rangeSpec}
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"int-input-" + name} id={"int-input-" + name}
/> />
</div> </div>
) : left === true && type === "prompt" ? ( </Case>
<Case condition={left === true && type === "prompt"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<PromptAreaComponent <PromptAreaComponent
readonly={data.node?.flow ? true : false} readonly={data.node?.flow ? true : false}
@ -717,57 +533,59 @@ export default function ParameterComponent({
setNodeClass={handleNodeClass} setNodeClass={handleNodeClass}
nodeClass={data.node} nodeClass={data.node}
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"prompt-input-" + name} id={"prompt-input-" + name}
data-testid={"prompt-input-" + name} data-testid={"prompt-input-" + name}
/> />
</div> </div>
) : left === true && type === "NestedDict" ? ( </Case>
<Case condition={left === true && type === "NestedDict"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<DictComponent <DictComponent
disabled={disabled} disabled={disabled}
editNode={false} editNode={false}
value={ value={
!data.node!.template[name].value || !data.node!.template[name]?.value ||
data.node!.template[name].value?.toString() === "{}" data.node!.template[name]?.value?.toString() === "{}"
? { ? {
// yourkey: "value", // yourkey: "value",
} }
: data.node!.template[name].value : data.node!.template[name]?.value
} }
onChange={handleOnNewValue} onChange={handleOnNewValue}
id="div-dict-input" id="div-dict-input"
/> />
</div> </div>
) : left === true && type === "dict" ? ( </Case>
<Case condition={left === true && type === "dict"}>
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<KeypairListComponent <KeypairListComponent
disabled={disabled} disabled={disabled}
editNode={false} editNode={false}
value={ value={
data.node!.template[name].value?.length === 0 || data.node!.template[name]?.value?.length === 0 ||
!data.node!.template[name].value !data.node!.template[name]?.value
? [{ "": "" }] ? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value) : convertObjToArray(data.node!.template[name]?.value, type!)
} }
duplicateKey={errorDuplicateKey} duplicateKey={errorDuplicateKey}
onChange={(newValue) => { onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue); const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers)); setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
// if data.node?.template[name].list is true, then the value is an array of objects // if data.node?.template[name]?.list is true, then the value is an array of objects
// else we need to get the first object of the array // else we need to get the first object of the array
if (data.node?.template[name].list) { if (data.node?.template[name]?.list) {
handleOnNewValue(valueToNumbers); handleOnNewValue(valueToNumbers);
} else handleOnNewValue(valueToNumbers[0]); } else handleOnNewValue(valueToNumbers[0]);
}} }}
isList={data.node?.template[name].list ?? false} isList={data.node?.template[name]?.list ?? false}
/> />
</div> </div>
) : ( </Case>
<></>
)}
</> </>
</div> </div>
); );

View file

@ -0,0 +1,91 @@
import React from "react";
import {
INPUT_HANDLER_HOVER,
OUTPUT_HANDLER_HOVER,
} from "../../../../constants/constants";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils";
const TooltipRenderComponent = ({ item, index, left }) => {
const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<div
key={index}
data-testid={`available-${left ? "input" : "output"}-${item.family}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span
className="ps-2 text-xs text-foreground"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs" data-testid={`tooltip-${item?.type}`}>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
};
export default TooltipRenderComponent;

View file

@ -54,14 +54,14 @@ export default function GenericNode({
const [nodeName, setNodeName] = useState(data.node!.display_name); const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false); const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState( const [nodeDescription, setNodeDescription] = useState(
data.node?.description! data.node?.description!,
); );
const [isOutdated, setIsOutdated] = useState(false); const [isOutdated, setIsOutdated] = useState(false);
const buildStatus = useFlowStore( const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status (state) => state.flowBuildStatus[data.id]?.status,
); );
const lastRunTime = useFlowStore( const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp (state) => state.flowBuildStatus[data.id]?.timestamp,
); );
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
@ -118,7 +118,7 @@ export default function GenericNode({
updateNodeInternals(data.id); updateNodeInternals(data.id);
}, },
[data.id, data.node, setNode, setIsOutdated] [data.id, data.node, setNode, setIsOutdated],
); );
if (!data.node!.template) { if (!data.node!.template) {
@ -258,7 +258,7 @@ export default function GenericNode({
const isDark = useDarkStore((state) => state.dark); const isDark = useDarkStore((state) => state.dark);
const renderIconStatus = ( const renderIconStatus = (
buildStatus: BuildStatus | undefined, buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null validationStatus: validationStatusType | null,
) => { ) => {
if (buildStatus === BuildStatus.BUILDING) { if (buildStatus === BuildStatus.BUILDING) {
return <Loading className="text-medium-indigo" />; return <Loading className="text-medium-indigo" />;
@ -299,11 +299,11 @@ export default function GenericNode({
}; };
const getSpecificClassFromBuildStatus = ( const getSpecificClassFromBuildStatus = (
buildStatus: BuildStatus | undefined, buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null validationStatus: validationStatusType | null,
) => { ) => {
let isInvalid = validationStatus && !validationStatus.valid; let isInvalid = validationStatus && !validationStatus.valid;
if (buildStatus === BuildStatus.INACTIVE && isInvalid) { if (buildStatus === BuildStatus.INACTIVE) {
// INACTIVE should have its own class // INACTIVE should have its own class
return "inactive-status"; return "inactive-status";
} }
@ -323,19 +323,20 @@ export default function GenericNode({
selected: boolean, selected: boolean,
showNode: boolean, showNode: boolean,
buildStatus: BuildStatus | undefined, buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null validationStatus: validationStatusType | null,
) => { ) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus( const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus, buildStatus,
validationStatus validationStatus,
); );
const baseBorderClass = getBaseBorderClass(selected); const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode); const nodeSizeClass = getNodeSizeClass(showNode);
return classNames( return classNames(
baseBorderClass, baseBorderClass,
nodeSizeClass, nodeSizeClass,
"generic-node-div", "generic-node-div",
specificClassFromBuildStatus specificClassFromBuildStatus,
); );
}; };
@ -383,7 +384,6 @@ export default function GenericNode({
isOutdated, isOutdated,
selected, selected,
]); ]);
return ( return (
<> <>
{memoizedNodeToolbarComponent} {memoizedNodeToolbarComponent}
@ -392,7 +392,7 @@ export default function GenericNode({
selected, selected,
showNode, showNode,
buildStatus, buildStatus,
validationStatus validationStatus,
)} )}
> >
{data.node?.beta && showNode && ( {data.node?.beta && showNode && (
@ -537,7 +537,7 @@ export default function GenericNode({
} }
title={getFieldTitle( title={getFieldTitle(
data.node?.template!, data.node?.template!,
templateField templateField,
)} )}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
@ -565,7 +565,7 @@ export default function GenericNode({
proxy={data.node?.template[templateField].proxy} proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
/> />
) ),
)} )}
<ParameterComponent <ParameterComponent
key={scapedJSONStringfy({ key={scapedJSONStringfy({
@ -722,7 +722,7 @@ export default function GenericNode({
!data.node?.description) && !data.node?.description) &&
nameEditable nameEditable
? "font-light italic" ? "font-light italic"
: "" : "",
)} )}
onDoubleClick={(e) => { onDoubleClick={(e) => {
setInputDescription(true); setInputDescription(true);
@ -784,13 +784,13 @@ export default function GenericNode({
} }
title={getFieldTitle( title={getFieldTitle(
data.node?.template!, data.node?.template!,
templateField templateField,
)} )}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
tooltipTitle={ tooltipTitle={
data.node?.template[templateField].input_types?.join( data.node?.template[templateField].input_types?.join(
"\n" "\n",
) ?? data.node?.template[templateField].type ) ?? data.node?.template[templateField].type
} }
required={data.node!.template[templateField].required} required={data.node!.template[templateField].required}
@ -817,7 +817,7 @@ export default function GenericNode({
<div <div
className={classNames( className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "", Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center" "flex-max-width justify-center",
)} )}
> >
{" "} {" "}

View file

@ -0,0 +1,54 @@
import { cloneDeep } from "lodash";
import { useEffect } from "react";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
const useFetchDataOnMount = (
data,
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
async function fetchData() {
if (
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []); // Empty dependency array ensures that this effect runs only once, on mount
};
export default useFetchDataOnMount;

View file

@ -0,0 +1,75 @@
import { cloneDeep } from "lodash";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorTypeAPI } from "../../types/api";
const useHandleOnNewValue = (
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleOnNewValue = async (newValue, skipSnapshot = false) => {
const nodeTemplate = data.node!.template[name];
const currentValue = nodeTemplate.value;
if (currentValue !== newValue && !skipSnapshot) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
currentValue !== newValue;
const typeToDebounce = nodeTemplate.type;
nodeTemplate.value = newValue;
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
if (["int"].includes(typeToDebounce)) {
newTemplate = await handleUpdateValues(name, data);
} else {
newTemplate = await debouncedHandleUpdateValues(name, data);
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
}
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else {
newNode.data.node.template[name].value = newValue;
}
return newNode;
});
renderTooltips();
};
return { handleOnNewValue };
};
export default useHandleOnNewValue;

View file

@ -0,0 +1,40 @@
import { cloneDeep } from "lodash";
const useHandleNodeClass = (
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
) => {
const handleNodeClass = (newNodeClass, code) => {
if (!data.node) return;
if (data.node!.template[name].value !== code) {
takeSnapshot();
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
newNode.data.node.template[name].value = code;
return newNode;
});
updateNodeInternals(data.id);
renderTooltips();
};
return { handleNodeClass };
};
export default useHandleNodeClass;

View file

@ -0,0 +1,39 @@
import { cloneDeep } from "lodash";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import { handleUpdateValues } from "../../utils/parameterUtils";
const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
};
return { handleRefreshButtonPress };
};
export default useHandleRefreshButtonPress;

View file

@ -114,19 +114,19 @@ export default function ChatMessage({
<div <div
className={classNames( className={classNames(
"form-modal-chat-position", "form-modal-chat-position",
chat.isSend ? "" : " " chat.isSend ? "" : " ",
)} )}
> >
<div <div
className={classNames( className={classNames(
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3" "mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3",
)} )}
> >
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<div <div
className={cn( className={cn(
"relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl", "relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl",
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon" !chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon",
)} )}
> >
<img <img
@ -210,12 +210,12 @@ dark:prose-invert"
children[0] = (children[0] as string).replace( children[0] = (children[0] as string).replace(
"`▍`", "`▍`",
"▍" "▍",
); );
} }
const match = /language-(\w+)/.exec( const match = /language-(\w+)/.exec(
className || "" className || "",
); );
return !inline ? ( return !inline ? (
@ -230,7 +230,7 @@ dark:prose-invert"
language: (match && match[1]) || "", language: (match && match[1]) || "",
code: String(children).replace( code: String(children).replace(
/\n$/, /\n$/,
"" "",
), ),
}, },
]} ]}
@ -248,7 +248,7 @@ dark:prose-invert"
{chatMessage} {chatMessage}
</Markdown> </Markdown>
), ),
[chat.message, chatMessage] [chat.message, chatMessage],
)} )}
</div> </div>
{chat.files && ( {chat.files && (
@ -306,7 +306,7 @@ dark:prose-invert"
parts.push( parts.push(
<span className="chat-message-highlight"> <span className="chat-message-highlight">
{chat.message[match[1]]} {chat.message[match[1]]}
</span> </span>,
); );
} }
@ -324,7 +324,7 @@ dark:prose-invert"
</> </>
) : ( ) : (
<span <span
className="prose text-primary word-break-break-word dark:prose-invert" className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={ data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage "chat-message-" + chat.sender_name + "-" + chatMessage
} }

View file

@ -201,6 +201,7 @@ const ApiModal = forwardRef(
}} }}
activeTweaks={activeTweaks} activeTweaks={activeTweaks}
setActiveTweaks={setActiveTweaks} setActiveTweaks={setActiveTweaks}
allowExport
/> />
</BaseModal.Content> </BaseModal.Content>
</BaseModal> </BaseModal>

View file

@ -28,6 +28,7 @@ import {
LANGFLOW_SUPPORTED_TYPES, LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal, limitScrollFieldsModal,
} from "../../constants/constants"; } from "../../constants/constants";
import { Case } from "../../shared/components/caseComponent";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { import {
@ -52,7 +53,7 @@ const EditNodeModal = forwardRef(
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
}, },
ref ref,
) => { ) => {
const [myData, setMyData] = useState(data); const [myData, setMyData] = useState(data);
@ -84,6 +85,10 @@ const EditNodeModal = forwardRef(
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const type = (templateParam) => {
return myData.node?.template[templateParam].type;
};
return ( return (
<BaseModal <BaseModal
key={data.id} key={data.id}
@ -116,7 +121,7 @@ const EditNodeModal = forwardRef(
"edit-node-modal-box", "edit-node-modal-box",
nodeLength > limitScrollFieldsModal nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll" ? "overflow-scroll overflow-x-hidden custom-scroll"
: "" : "",
)} )}
> >
{nodeLength > 0 && ( {nodeLength > 0 && (
@ -138,8 +143,8 @@ const EditNodeModal = forwardRef(
templateParam.charAt(0) !== "_" && templateParam.charAt(0) !== "_" &&
myData.node?.template[templateParam].show && myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has( LANGFLOW_SUPPORTED_TYPES.has(
myData.node.template[templateParam].type myData.node!.template[templateParam].type,
) ),
) )
.map((templateParam, index) => { .map((templateParam, index) => {
let id = { let id = {
@ -161,8 +166,8 @@ const EditNodeModal = forwardRef(
myData.node?.template[templateParam] myData.node?.template[templateParam]
.proxy, .proxy,
} }
: id : id,
) ),
) ?? false; ) ?? false;
return ( return (
<TableRow <TableRow
@ -170,8 +175,7 @@ const EditNodeModal = forwardRef(
className={ className={
"h-10 " + "h-10 " +
((templateParam === "code" && ((templateParam === "code" &&
myData.node?.template[templateParam].type === type(templateParam) === "code") ||
"code") ||
(templateParam.includes("code") && (templateParam.includes("code") &&
myData.node?.template[templateParam].proxy) myData.node?.template[templateParam].proxy)
? " hidden " ? " hidden "
@ -190,7 +194,7 @@ const EditNodeModal = forwardRef(
<span> <span>
{myData.node?.template[templateParam] {myData.node?.template[templateParam]
.display_name .display_name
? myData.node.template[templateParam] ? myData.node!.template[templateParam]
.display_name .display_name
: myData.node?.template[templateParam] : myData.node?.template[templateParam]
.name} .name}
@ -198,58 +202,62 @@ const EditNodeModal = forwardRef(
</ShadTooltip> </ShadTooltip>
</TableCell> </TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground "> <TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.node?.template[templateParam].type === <Case
"str" && condition={
!myData.node.template[templateParam].options ? ( type(templateParam) === "str" &&
!myData.node!.template[templateParam]
.options
}
>
<div className="mx-auto"> <div className="mx-auto">
{myData.node.template[templateParam] {myData.node!.template[templateParam]
?.list ? ( ?.list ? (
<InputListComponent <InputListComponent
componentName={templateParam} componentName={templateParam}
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
value={ value={
!myData.node.template[templateParam] !myData.node!.template[templateParam]
.value || .value ||
myData.node.template[templateParam] myData.node!.template[templateParam]
.value === "" .value === ""
? [""] ? [""]
: myData.node.template[ : myData.node!.template[
templateParam templateParam
].value ].value
} }
onChange={(value: string[]) => { onChange={(value: string[]) => {
handleOnNewValue( handleOnNewValue(
value, value,
templateParam templateParam,
); );
}} }}
/> />
) : myData.node.template[templateParam] ) : myData.node!.template[templateParam]
.multiline ? ( .multiline ? (
<TextAreaComponent <TextAreaComponent
id={ id={
"textarea-edit-" + "textarea-edit-" +
myData.node.template[templateParam] myData.node!.template[templateParam]
.name .name
} }
data-testid={ data-testid={
"textarea-edit-" + "textarea-edit-" +
myData.node.template[templateParam] myData.node!.template[templateParam]
.name .name
} }
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "" .value ?? ""
} }
onChange={( onChange={(
value: string | string[] value: string | string[],
) => { ) => {
handleOnNewValue( handleOnNewValue(
value, value,
templateParam templateParam,
); );
}} }}
/> />
@ -274,8 +282,13 @@ const EditNodeModal = forwardRef(
/> />
)} )}
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "NestedDict" ? (
<Case
condition={
type(templateParam) === "NestedDict"
}
>
<div className=" w-full"> <div className=" w-full">
<DictComponent <DictComponent
disabled={disabled} disabled={disabled}
@ -296,21 +309,24 @@ const EditNodeModal = forwardRef(
].value = newValue; ].value = newValue;
handleOnNewValue( handleOnNewValue(
newValue, newValue,
templateParam templateParam,
); );
}} }}
id="editnode-div-dict-input" id="editnode-div-dict-input"
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "dict" ? (
<Case
condition={type(templateParam) === "dict"}
>
<div <div
className={classNames( className={classNames(
"max-h-48 w-full overflow-auto custom-scroll", "max-h-48 w-full overflow-auto custom-scroll",
myData.node!.template[templateParam].value myData.node!.template[templateParam].value
?.length > 1 ?.length > 1
? "my-3" ? "my-3"
: "" : "",
)} )}
> >
<KeypairListComponent <KeypairListComponent
@ -325,7 +341,8 @@ const EditNodeModal = forwardRef(
: convertObjToArray( : convertObjToArray(
myData.node!.template[ myData.node!.template[
templateParam templateParam
].value ].value,
type(templateParam)!,
) )
} }
duplicateKey={errorDuplicateKey} duplicateKey={errorDuplicateKey}
@ -336,11 +353,11 @@ const EditNodeModal = forwardRef(
templateParam templateParam
].value = valueToNumbers; ].value = valueToNumbers;
setErrorDuplicateKey( setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers) hasDuplicateKeys(valueToNumbers),
); );
handleOnNewValue( handleOnNewValue(
valueToNumbers, valueToNumbers,
templateParam templateParam,
); );
}} }}
isList={ isList={
@ -349,32 +366,39 @@ const EditNodeModal = forwardRef(
} }
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "bool" ? (
<Case
condition={type(templateParam) === "bool"}
>
<div className="ml-auto"> <div className="ml-auto">
{" "} {" "}
<ToggleShadComponent <ToggleShadComponent
id={ id={
"toggle-edit-" + "toggle-edit-" +
myData.node.template[templateParam].name myData.node!.template[templateParam]
.name
} }
disabled={disabled} disabled={disabled}
enabled={ enabled={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value .value
} }
setEnabled={(isEnabled) => { setEnabled={(isEnabled) => {
handleOnNewValue( handleOnNewValue(
isEnabled, isEnabled,
templateParam templateParam,
); );
}} }}
size="small" size="small"
editNode={true} editNode={true}
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "float" ? (
<Case
condition={type(templateParam) === "float"}
>
<div className="mx-auto"> <div className="mx-auto">
<FloatComponent <FloatComponent
disabled={disabled} disabled={disabled}
@ -384,7 +408,7 @@ const EditNodeModal = forwardRef(
.rangeSpec .rangeSpec
} }
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "" .value ?? ""
} }
onChange={(value) => { onChange={(value) => {
@ -392,32 +416,38 @@ const EditNodeModal = forwardRef(
}} }}
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "str" &&
myData.node.template[templateParam] <Case
.options ? ( condition={
type(templateParam) === "str" &&
myData.node!.template[templateParam].options
}
>
<div className="mx-auto"> <div className="mx-auto">
<Dropdown <Dropdown
editNode={true} editNode={true}
options={ options={
myData.node.template[templateParam] myData.node!.template[templateParam]
.options .options
} }
onSelect={(value) => onSelect={(value) =>
handleOnNewValue(value, templateParam) handleOnNewValue(value, templateParam)
} }
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "Choose an option" .value ?? "Choose an option"
} }
id={ id={
"dropdown-edit-" + "dropdown-edit-" +
myData.node.template[templateParam].name myData.node!.template[templateParam]
.name
} }
></Dropdown> ></Dropdown>
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "int" ? (
<Case condition={type(templateParam) === "int"}>
<div className="mx-auto"> <div className="mx-auto">
<IntComponent <IntComponent
rangeSpec={ rangeSpec={
@ -426,12 +456,13 @@ const EditNodeModal = forwardRef(
} }
id={ id={
"edit-int-input-" + "edit-int-input-" +
myData.node.template[templateParam].name myData.node!.template[templateParam]
.name
} }
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "" .value ?? ""
} }
onChange={(value) => { onChange={(value) => {
@ -439,21 +470,24 @@ const EditNodeModal = forwardRef(
}} }}
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "file" ? (
<Case
condition={type(templateParam) === "file"}
>
<div className="mx-auto"> <div className="mx-auto">
<InputFileComponent <InputFileComponent
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "" .value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
fileTypes={ fileTypes={
myData.node.template[templateParam] myData.node!.template[templateParam]
.fileTypes .fileTypes
} }
onFileChange={(filePath: string) => { onFileChange={(filePath: string) => {
@ -463,8 +497,11 @@ const EditNodeModal = forwardRef(
}} }}
></InputFileComponent> ></InputFileComponent>
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "prompt" ? (
<Case
condition={type(templateParam) === "prompt"}
>
<div className="mx-auto"> <div className="mx-auto">
<PromptAreaComponent <PromptAreaComponent
readonly={ readonly={
@ -478,7 +515,7 @@ const EditNodeModal = forwardRef(
myData.node = nodeClass; myData.node = nodeClass;
}} }}
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "" .value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
@ -486,21 +523,26 @@ const EditNodeModal = forwardRef(
}} }}
id={ id={
"prompt-area-edit-" + "prompt-area-edit-" +
myData.node.template[templateParam].name myData.node!.template[templateParam]
.name
} }
data-testid={ data-testid={
"modal-prompt-input-" + "modal-prompt-input-" +
myData.node.template[templateParam].name myData.node!.template[templateParam]
.name
} }
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "code" ? (
<Case
condition={type(templateParam) === "code"}
>
<div className="mx-auto"> <div className="mx-auto">
<CodeAreaComponent <CodeAreaComponent
readonly={ readonly={
myData.node?.flow && myData.node?.flow &&
myData.node.template[templateParam] myData.node!.template[templateParam]
.dynamic .dynamic
? true ? true
: false : false
@ -516,7 +558,7 @@ const EditNodeModal = forwardRef(
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.node.template[templateParam] myData.node!.template[templateParam]
.value ?? "" .value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
@ -524,16 +566,16 @@ const EditNodeModal = forwardRef(
}} }}
id={ id={
"code-area-edit" + "code-area-edit" +
myData.node.template[templateParam].name myData.node!.template[templateParam]
.name
} }
/> />
</div> </div>
) : myData.node?.template[templateParam] </Case>
.type === "Any" ? (
"-" <Case condition={type(templateParam) === "Any"}>
) : ( <>-</>
<div className="hidden"></div> </Case>
)}
</TableCell> </TableCell>
<TableCell className="p-0 text-right"> <TableCell className="p-0 text-right">
<div className="items-center text-center"> <div className="items-center text-center">
@ -588,7 +630,7 @@ const EditNodeModal = forwardRef(
</BaseModal.Footer> </BaseModal.Footer>
</BaseModal> </BaseModal>
); );
} },
); );
export default EditNodeModal; export default EditNodeModal;

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow"; import { NodeToolbar } from "reactflow";
import { GradientGroup } from "../../../../icons/GradientSparkles"; import { GradientGroup } from "../../../../icons/GradientSparkles";
import { validateSelection } from "../../../../utils/reactflowUtils";
import useFlowStore from "../../../../stores/flowStore"; import useFlowStore from "../../../../stores/flowStore";
import { validateSelection } from "../../../../utils/reactflowUtils";
export default function SelectionMenu({ export default function SelectionMenu({
onClick, onClick,
nodes, nodes,
@ -62,7 +62,11 @@ export default function SelectionMenu({
} }
> >
<button <button
className={`${disable ? "flex h-full w-full cursor-not-allowed items-center justify-between text-sm text-muted-foreground" : "flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"}`} className={`${
disable
? "flex h-full w-full cursor-not-allowed items-center justify-between text-sm text-muted-foreground"
: "flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
}`}
onClick={onClick} onClick={onClick}
disabled={disable} disabled={disable}
> >

View file

@ -3,14 +3,14 @@ import { useParams } from "react-router-dom";
import FlowToolbar from "../../components/chatComponent"; import FlowToolbar from "../../components/chatComponent";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import Page from "./components/PageComponent"; import Page from "./components/PageComponent";
import ExtraSidebar from "./components/extraSidebarComponent"; import ExtraSidebar from "./components/extraSidebarComponent";
import useFlowStore from "../../stores/flowStore";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element { export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore( const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId (state) => state.setCurrentFlowId,
); );
const version = useDarkStore((state) => state.version); const version = useDarkStore((state) => state.version);
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage); const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);

View file

@ -1,64 +1,69 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { getComponent } from "../../controllers/API";
import cloneFLowWithParent from "../../utils/storeUtils";
import LoadingComponent from "../../components/loadingComponent"; import LoadingComponent from "../../components/loadingComponent";
import useFlowStore from "../../stores/flowStore"; import { getComponent } from "../../controllers/API";
import IOModal from "../../modals/IOModal"; import IOModal from "../../modals/IOModal";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import cloneFLowWithParent from "../../utils/storeUtils";
export default function PlaygroundPage() { export default function PlaygroundPage() {
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const getFlowById = useFlowsManagerStore((state) => state.getFlowById); const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const setCurrentFlowId = useFlowsManagerStore((state) => state.setCurrentFlowId); const setCurrentFlowId = useFlowsManagerStore(
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); (state) => state.setCurrentFlowId,
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow); );
const setNodes = useFlowStore((state) => state.setNodes); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setEdges = useFlowStore((state) => state.setEdges); const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool); const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
const { id } = useParams(); const { id } = useParams();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
async function getFlowData() { async function getFlowData() {
const res = await getComponent(id!); const res = await getComponent(id!);
const newFlow = cloneFLowWithParent(res, res.id, false, true); const newFlow = cloneFLowWithParent(res, res.id, false, true);
return newFlow; return newFlow;
}
// Set flow tab id
useEffect(() => {
console.log("id", id);
if (getFlowById(id!)) {
setCurrentFlowId(id!);
} else {
getFlowData().then((flow) => {
setCurrentFlow(flow);
});
} }
}, [id]);
// Set flow tab id useEffect(() => {
useEffect(() => { if (currentFlow) {
console.log("id", id); setNodes(currentFlow?.data?.nodes ?? [], true);
if (getFlowById(id!)) { setEdges(currentFlow?.data?.edges ?? [], true);
setCurrentFlowId(id!); cleanFlowPool();
} setLoading(false);
else { }
getFlowData().then((flow) => { return () => {
setCurrentFlow(flow); setNodes([], true);
}); setEdges([], true);
} cleanFlowPool();
}, [id]); };
}, [currentFlow]);
useEffect(() => { return (
if (currentFlow) { <div className="flex h-full w-full flex-col items-center justify-center align-middle">
setNodes(currentFlow?.data?.nodes ?? [], true); {loading ? (
setEdges(currentFlow?.data?.edges ?? [], true); <div>
cleanFlowPool(); <LoadingComponent remSize={24}></LoadingComponent>
setLoading(false);
}
return () => {
setNodes([], true);
setEdges([], true);
cleanFlowPool();
};
}, [currentFlow]);
return (
<div className="w-full h-full flex flex-col align-middle items-center justify-center">
{loading ? <div><LoadingComponent remSize={24}></LoadingComponent></div> :
<IOModal open={true}setOpen={()=>{}} isPlayground>
<></>
</IOModal>}
</div> </div>
) ) : (
<IOModal open={true} setOpen={() => {}} isPlayground>
<></>
</IOModal>
)}
</div>
);
} }

View file

@ -32,11 +32,11 @@ import { gradients } from "../../../../utils/styleUtils";
export default function GeneralPage() { export default function GeneralPage() {
const setCurrentFlowId = useFlowsManagerStore( const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId (state) => state.setCurrentFlowId,
); );
const [inputState, setInputState] = useState<patchUserInputStateType>( const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE CONTROL_PATCH_USER_STATE,
); );
const { autoLogin } = useContext(AuthContext); const { autoLogin } = useContext(AuthContext);
@ -113,110 +113,109 @@ export default function GeneralPage() {
</div> </div>
<div className="grid gap-6"> <div className="grid gap-6">
<Form.Root
<Form.Root onSubmit={(event) => {
onSubmit={(event) => { handlePatchGradient();
handlePatchGradient(); event.preventDefault();
event.preventDefault(); }}
}} >
> <Card x-chunk="dashboard-04-chunk-1">
<Card x-chunk="dashboard-04-chunk-1"> <CardHeader>
<CardHeader> <CardTitle>Profile Gradient</CardTitle>
<CardTitle>Profile Gradient</CardTitle> <CardDescription>
<CardDescription> Choose the gradient that appears as your profile picture.
Choose the gradient that appears as your profile picture. </CardDescription>
</CardDescription> </CardHeader>
</CardHeader> <CardContent>
<CardContent> <div className="py-2">
<div className="py-2"> <GradientChooserComponent
<GradientChooserComponent value={
value={ gradient == ""
gradient == "" ? userData?.profile_image ??
? userData?.profile_image ?? gradients[
gradients[ parseInt(userData?.id ?? "", 30) % gradients.length
parseInt(userData?.id ?? "", 30) % ]
gradients.length : gradient
] }
: gradient onChange={(value) => {
} handleInput({ target: { name: "gradient", value } });
}}
/>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
{!autoLogin && (
<Form.Root
onSubmit={(event) => {
handlePatchPassword();
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Type your new password and confirm it.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full gap-4">
<Form.Field name="password" className="w-full">
<InputComponent
id="pasword"
onChange={(value) => { onChange={(value) => {
handleInput({ target: { name: "gradient", value } }); handleInput({ target: { name: "password", value } });
}} }}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message
match="valueMissing"
className="field-invalid"
>
Please enter your password
</Form.Message>
</Form.Field>
<Form.Field name="cnfPassword" className="w-full">
<InputComponent
id="cnfPassword"
onChange={(value) => {
handleInput({
target: { name: "cnfPassword", value },
});
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/> />
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>{!autoLogin && (
<Form.Root
onSubmit={(event) => {
handlePatchPassword();
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Type your new password and confirm it.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex w-full gap-4">
<Form.Field name="password" className="w-full">
<InputComponent
id="pasword"
onChange={(value) => {
handleInput({ target: { name: "password", value } });
}}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message
match="valueMissing"
className="field-invalid"
>
Please enter your password
</Form.Message>
</Form.Field>
<Form.Field name="cnfPassword" className="w-full">
<InputComponent
id="cnfPassword"
onChange={(value) => {
handleInput({
target: { name: "cnfPassword", value },
});
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/>
<Form.Message <Form.Message
className="field-invalid" className="field-invalid"
match="valueMissing" match="valueMissing"
> >
Please confirm your password Please confirm your password
</Form.Message> </Form.Message>
</Form.Field> </Form.Field>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="border-t px-6 py-4"> <CardFooter className="border-t px-6 py-4">
<Form.Submit asChild> <Form.Submit asChild>
<Button type="submit">Save</Button> <Button type="submit">Save</Button>
</Form.Submit> </Form.Submit>
</CardFooter> </CardFooter>
</Card> </Card>
</Form.Root> </Form.Root>
)} )}
</div> </div>
</div> </div>

View file

@ -0,0 +1,15 @@
import { memo } from "react";
type BooleanLike = boolean | string | number | null | undefined;
type Props = {
condition: (() => BooleanLike) | BooleanLike;
children: React.ReactNode | any;
};
export const Case = memo(({ condition, children }: Props) => {
const conditionResult =
typeof condition === "function" ? condition() : condition;
return conditionResult ? children : null;
});

View file

@ -44,7 +44,6 @@ import { getInputsAndOutputs } from "../utils/storeUtils";
import useAlertStore from "./alertStore"; import useAlertStore from "./alertStore";
import { useDarkStore } from "./darkStore"; import { useDarkStore } from "./darkStore";
import useFlowsManagerStore from "./flowsManagerStore"; import useFlowsManagerStore from "./flowsManagerStore";
import FlowPage from "../pages/FlowPage";
// this is our useStore hook that we can use in our components to get parts of the store and call actions // this is our useStore hook that we can use in our components to get parts of the store and call actions
const useFlowStore = create<FlowStoreType>((set, get) => ({ const useFlowStore = create<FlowStoreType>((set, get) => ({
@ -465,8 +464,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
status: BuildStatus, status: BuildStatus,
runId: string, runId: string,
) { ) {
console.log("handleBuildUpdate", vertexBuildData, status, runId);
if (vertexBuildData && vertexBuildData.inactivated_vertices) { if (vertexBuildData && vertexBuildData.inactivated_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices); get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices);
get().updateBuildStatus(
vertexBuildData.inactivated_vertices,
BuildStatus.INACTIVE,
);
} }
if (vertexBuildData.next_vertices_ids) { if (vertexBuildData.next_vertices_ids) {
@ -483,9 +487,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const next_vertices_ids = vertexBuildData.next_vertices_ids.filter( const next_vertices_ids = vertexBuildData.next_vertices_ids.filter(
(id) => !vertexBuildData.inactivated_vertices?.includes(id), (id) => !vertexBuildData.inactivated_vertices?.includes(id),
); );
const top_level_vertices = vertexBuildData.top_level_vertices.filter(
(vertex) =>
!vertexBuildData.inactivated_vertices?.includes(vertex.id),
);
const nextVertices: VertexLayerElementType[] = zip( const nextVertices: VertexLayerElementType[] = zip(
next_vertices_ids, next_vertices_ids,
vertexBuildData.top_level_vertices, top_level_vertices,
).map(([id, reference]) => ({ id: id!, reference })); ).map(([id, reference]) => ({ id: id!, reference }));
const newLayers = [ const newLayers = [
@ -502,10 +510,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
runId: runId, runId: runId,
verticesToRun: get().verticesBuild!.verticesToRun, verticesToRun: get().verticesBuild!.verticesToRun,
}); });
get().updateBuildStatus( get().updateBuildStatus(top_level_vertices, BuildStatus.TO_BUILD);
vertexBuildData.top_level_vertices,
BuildStatus.TO_BUILD,
);
} }
get().addDataToFlowPool( get().addDataToFlowPool(

View file

@ -52,7 +52,6 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
currentFlow: flow, currentFlow: flow,
currentFlowId: flow.id, currentFlowId: flow.id,
})); }));
}, },
getFlowById: (id: string) => { getFlowById: (id: string) => {
return get().flows.find((flow) => flow.id === id); return get().flows.find((flow) => flow.id === id);
@ -83,13 +82,13 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
const { data, flows } = processFlows(dbData, false); const { data, flows } = processFlows(dbData, false);
get().setExamples( get().setExamples(
flows.filter( flows.filter(
(f) => f.folder === STARTER_FOLDER_NAME && !f.user_id (f) => f.folder === STARTER_FOLDER_NAME && !f.user_id,
) ),
); );
get().setFlows( get().setFlows(
flows.filter( flows.filter(
(f) => !(f.folder === STARTER_FOLDER_NAME && !f.user_id) (f) => !(f.folder === STARTER_FOLDER_NAME && !f.user_id),
) ),
); );
useTypesStore.setState((state) => ({ useTypesStore.setState((state) => ({
data: { ...state.data, ["saved_components"]: data }, data: { ...state.data, ["saved_components"]: data },
@ -115,7 +114,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (get().currentFlow) { if (get().currentFlow) {
get().saveFlow( get().saveFlow(
{ ...get().currentFlow!, data: { nodes, edges, viewport } }, { ...get().currentFlow!, data: { nodes, edges, viewport } },
true true,
); );
} }
}, },
@ -141,7 +140,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return updatedFlow; return updatedFlow;
} }
return flow; return flow;
}) }),
); );
//update tabs state //update tabs state
@ -191,7 +190,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
newProject: Boolean, newProject: Boolean,
flow?: FlowType, flow?: FlowType,
override?: boolean, override?: boolean,
position?: XYPosition position?: XYPosition,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
if (newProject) { if (newProject) {
let flowData = flow let flowData = flow
@ -255,7 +254,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
.getState() .getState()
.paste( .paste(
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges }, { nodes: flow!.data!.nodes, edges: flow!.data!.edges },
position ?? { x: 10, y: 10 } position ?? { x: 10, y: 10 },
); );
} }
}, },
@ -265,7 +264,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (index >= 0) { if (index >= 0) {
deleteFlowFromDatabase(id).then(() => { deleteFlowFromDatabase(id).then(() => {
const { data, flows } = processFlows( const { data, flows } = processFlows(
get().flows.filter((flow) => flow.id !== id) get().flows.filter((flow) => flow.id !== id),
); );
get().setFlows(flows); get().setFlows(flows);
set({ isLoading: false }); set({ isLoading: false });
@ -285,7 +284,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
let componentFlow = get().flows.find( let componentFlow = get().flows.find(
(componentFlow) => (componentFlow) =>
componentFlow.is_component && componentFlow.name === key componentFlow.is_component && componentFlow.name === key,
); );
if (componentFlow) { if (componentFlow) {
@ -367,7 +366,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return get().addFlow( return get().addFlow(
true, true,
createFlowComponent(component, useDarkStore.getState().version), createFlowComponent(component, useDarkStore.getState().version),
override override,
); );
}, },
takeSnapshot: () => { takeSnapshot: () => {
@ -388,7 +387,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
if (pastLength > 0) { if (pastLength > 0) {
past[currentFlowId] = past[currentFlowId].slice( past[currentFlowId] = past[currentFlowId].slice(
pastLength - defaultOptions.maxHistorySize + 1, pastLength - defaultOptions.maxHistorySize + 1,
pastLength pastLength,
); );
past[currentFlowId].push(newState); past[currentFlowId].push(newState);

View file

@ -400,6 +400,7 @@ export type StoreApiKeyType = {
export type groupedObjType = { export type groupedObjType = {
family: string; family: string;
type: string; type: string;
display_name?: string;
}; };
export type nodeGroupedObjType = { export type nodeGroupedObjType = {
@ -663,6 +664,7 @@ export type codeTabsPropsType = {
}; };
activeTweaks?: boolean; activeTweaks?: boolean;
setActiveTweaks?: (value: boolean) => void; setActiveTweaks?: (value: boolean) => void;
allowExport?: boolean;
}; };
export type crashComponentPropsType = { export type crashComponentPropsType = {

View file

@ -70,26 +70,32 @@ export type FlowStoreType = {
state: state:
| FlowState | FlowState
| undefined | undefined
| ((oldState: FlowState | undefined) => FlowState) | ((oldState: FlowState | undefined) => FlowState),
) => void; ) => void;
nodes: Node[]; nodes: Node[];
edges: Edge[]; edges: Edge[];
onNodesChange: OnNodesChange; onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange; onEdgesChange: OnEdgesChange;
setNodes: (update: Node[] | ((oldState: Node[]) => Node[]),skipSave?:boolean) => void; setNodes: (
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[]),skipSave?:boolean) => void; update: Node[] | ((oldState: Node[]) => Node[]),
skipSave?: boolean,
) => void;
setEdges: (
update: Edge[] | ((oldState: Edge[]) => Edge[]),
skipSave?: boolean,
) => void;
setNode: (id: string, update: Node | ((oldState: Node) => Node)) => void; setNode: (id: string, update: Node | ((oldState: Node) => Node)) => void;
getNode: (id: string) => Node | undefined; getNode: (id: string) => Node | undefined;
deleteNode: (nodeId: string | Array<string>) => void; deleteNode: (nodeId: string | Array<string>) => void;
deleteEdge: (edgeId: string | Array<string>) => void; deleteEdge: (edgeId: string | Array<string>) => void;
paste: ( paste: (
selection: { nodes: any; edges: any }, selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number },
) => void; ) => void;
lastCopiedSelection: { nodes: any; edges: any } | null; lastCopiedSelection: { nodes: any; edges: any } | null;
setLastCopiedSelection: ( setLastCopiedSelection: (
newSelection: { nodes: any; edges: any } | null, newSelection: { nodes: any; edges: any } | null,
isCrop?: boolean isCrop?: boolean,
) => void; ) => void;
cleanFlow: () => void; cleanFlow: () => void;
setFilterEdge: (newState) => void; setFilterEdge: (newState) => void;
@ -113,7 +119,7 @@ export type FlowStoreType = {
verticesLayers: VertexLayerElementType[][]; verticesLayers: VertexLayerElementType[][];
runId: string; runId: string;
verticesToRun: string[]; verticesToRun: string[];
} | null } | null,
) => void; ) => void;
addToVerticesBuild: (vertices: string[]) => void; addToVerticesBuild: (vertices: string[]) => void;
removeFromVerticesBuild: (vertices: string[]) => void; removeFromVerticesBuild: (vertices: string[]) => void;
@ -131,7 +137,7 @@ export type FlowStoreType = {
updateFlowPool: ( updateFlowPool: (
nodeId: string, nodeId: string,
data: FlowPoolObjectType | ChatOutputType | chatInputType, data: FlowPoolObjectType | ChatOutputType | chatInputType,
buildId?: string buildId?: string,
) => void; ) => void;
getNodePosition: (nodeId: string) => { x: number; y: number }; getNodePosition: (nodeId: string) => { x: number; y: number };
}; };

View file

@ -21,11 +21,11 @@ export type GlobalVariablesStore = {
id: string, id: string,
type?: string, type?: string,
default_fields?: string[], default_fields?: string[],
value?: string value?: string,
) => void; ) => void;
removeGlobalVariable: (name: string) => Promise<void>; removeGlobalVariable: (name: string) => Promise<void>;
getVariableId: (name: string) => string | undefined; getVariableId: (name: string) => string | undefined;
unavaliableFields: {[name: string]: string}; unavaliableFields: { [name: string]: string };
setUnavaliableFields: (fields: {[name: string]: string}) => void; setUnavaliableFields: (fields: { [name: string]: string }) => void;
removeUnavaliableField: (field: string) => void; removeUnavaliableField: (field: string) => void;
}; };

View file

@ -457,7 +457,8 @@ export function getConnectedNodes(
return nodes.filter((node) => node.id === targetId || node.id === sourceId); return nodes.filter((node) => node.id === targetId || node.id === sourceId);
} }
export function convertObjToArray(singleObject: object | string) { export function convertObjToArray(singleObject: object | string, type: string) {
if (type !== "dict") return [{ "": "" }];
if (typeof singleObject === "string") { if (typeof singleObject === "string") {
singleObject = JSON.parse(singleObject); singleObject = JSON.parse(singleObject);
} }
@ -736,28 +737,30 @@ export function validateSelection(
selection: OnSelectionChangeParams, selection: OnSelectionChangeParams,
edges: Edge[], edges: Edge[],
): Array<string> { ): Array<string> {
const clonedSelection = cloneDeep(selection);
const clonedEdges = cloneDeep(edges);
//add edges to selection if selection mode selected only nodes //add edges to selection if selection mode selected only nodes
if (selection.edges.length === 0) { if (clonedSelection.edges.length === 0) {
selection.edges = edges; clonedSelection.edges = clonedEdges;
} }
// get only edges that are connected to the nodes in the selection // get only edges that are connected to the nodes in the selection
// first creates a set of all the nodes ids // first creates a set of all the nodes ids
let nodesSet = new Set(selection.nodes.map((n) => n.id)); let nodesSet = new Set(clonedSelection.nodes.map((n) => n.id));
// then filter the edges that are connected to the nodes in the set // then filter the edges that are connected to the nodes in the set
let connectedEdges = selection.edges.filter( let connectedEdges = clonedSelection.edges.filter(
(e) => nodesSet.has(e.source) && nodesSet.has(e.target), (e) => nodesSet.has(e.source) && nodesSet.has(e.target),
); );
// add the edges to the selection // add the edges to the selection
selection.edges = connectedEdges; clonedSelection.edges = connectedEdges;
let errorsArray: Array<string> = []; let errorsArray: Array<string> = [];
// check if there is more than one node // check if there is more than one node
if (selection.nodes.length < 2) { if (clonedSelection.nodes.length < 2) {
errorsArray.push("Please select more than one node"); errorsArray.push("Please select more than one node");
} }
if ( if (
selection.nodes.some( clonedSelection.nodes.some(
(node) => (node) =>
isInputNode(node.data as NodeDataType) || isInputNode(node.data as NodeDataType) ||
isOutputNode(node.data as NodeDataType), isOutputNode(node.data as NodeDataType),
@ -769,8 +772,8 @@ export function validateSelection(
} }
//check if there are two or more nodes with free outputs //check if there are two or more nodes with free outputs
if ( if (
selection.nodes.filter( clonedSelection.nodes.filter(
(n) => !selection.edges.some((e) => e.source === n.id), (n) => !clonedSelection.edges.some((e) => e.source === n.id),
).length > 1 ).length > 1
) { ) {
errorsArray.push("Please select only one node with free outputs"); errorsArray.push("Please select only one node with free outputs");
@ -778,10 +781,10 @@ export function validateSelection(
// check if there is any node that does not have any connection // check if there is any node that does not have any connection
if ( if (
selection.nodes.some( clonedSelection.nodes.some(
(node) => (node) =>
!selection.edges.some((edge) => edge.target === node.id) && !clonedSelection.edges.some((edge) => edge.target === node.id) &&
!selection.edges.some((edge) => edge.source === node.id), !clonedSelection.edges.some((edge) => edge.source === node.id),
) )
) { ) {
errorsArray.push("Please select only nodes that are connected"); errorsArray.push("Please select only nodes that are connected");

View file

@ -7,15 +7,14 @@ export default function cloneFLowWithParent(
flow: FlowType, flow: FlowType,
parent: string, parent: string,
is_component: boolean, is_component: boolean,
keepId=false keepId = false,
) { ) {
let childFLow = cloneDeep(flow); let childFLow = cloneDeep(flow);
childFLow.parent = parent; childFLow.parent = parent;
if(!keepId){ if (!keepId) {
childFLow.id = ""; childFLow.id = "";
} } else {
else{ childFLow.id = uniqueId() + "-" + childFLow.id;
childFLow.id = uniqueId()+"-"+childFLow.id;
} }
childFLow.is_component = is_component; childFLow.is_component = is_component;
return childFLow; return childFLow;
@ -23,7 +22,7 @@ export default function cloneFLowWithParent(
export function getTagsIds( export function getTagsIds(
tags: string[], tags: string[],
tagListId: { name: string; id: string }[] tagListId: { name: string; id: string }[],
) { ) {
return tags return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))! .map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!

View file

@ -143,6 +143,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { FaApple, FaGithub } from "react-icons/fa"; import { FaApple, FaGithub } from "react-icons/fa";
import { AWSIcon } from "../icons/AWS"; import { AWSIcon } from "../icons/AWS";
import { FaDiscord } from "react-icons/fa";
import { AirbyteIcon } from "../icons/Airbyte"; import { AirbyteIcon } from "../icons/Airbyte";
import { AnthropicIcon } from "../icons/Anthropic"; import { AnthropicIcon } from "../icons/Anthropic";
import { AstraDBIcon } from "../icons/AstraDB"; import { AstraDBIcon } from "../icons/AstraDB";
@ -517,4 +518,5 @@ export const nodeIconsLucide: iconsType = {
Command, Command,
ArrowBigUp, ArrowBigUp,
Dot, Dot,
Discord: FaDiscord,
}; };

View file

@ -190,6 +190,7 @@ export function getPythonApiCode(
isAuth: boolean, isAuth: boolean,
tweaksBuildedObject, tweaksBuildedObject,
): string { ): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests return `import requests
from typing import Optional from typing import Optional
@ -197,7 +198,7 @@ BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}" FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary # You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}} # e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaksBuildedObject, null, 2)} TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
def run_flow(message: str, def run_flow(message: str,
flow_id: str, flow_id: str,
@ -247,6 +248,8 @@ export function getCurlCode(
isAuth: boolean, isAuth: boolean,
tweaksBuildedObject, tweaksBuildedObject,
): string { ): string {
const tweaksObject = tweaksBuildedObject[0];
return `curl -X POST \\ return `curl -X POST \\
${window.location.protocol}//${ ${window.location.protocol}//${
window.location.host window.location.host
@ -257,7 +260,7 @@ export function getCurlCode(
-d '{"input_value": "message", -d '{"input_value": "message",
"output_type": "chat", "output_type": "chat",
"input_type": "chat", "input_type": "chat",
"tweaks": ${JSON.stringify(tweaksBuildedObject, null, 2)}' "tweaks": ${JSON.stringify(tweaksObject, null, 2)}'
`; `;
} }
@ -285,11 +288,14 @@ export function getOutputIds(flow) {
* @returns {string} - The python code * @returns {string} - The python code
*/ */
export function getPythonCode(flowName: string, tweaksBuildedObject): string { export function getPythonCode(flowName: string, tweaksBuildedObject): string {
const tweaksObject = tweaksBuildedObject[0];
return `from langflow.load import run_flow_from_json return `from langflow.load import run_flow_from_json
TWEAKS = ${JSON.stringify(tweaksBuildedObject, null, 2)} TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
result = run_flow_from_json(flow="${flowName}.json", result = run_flow_from_json(flow="${flowName}.json",
input_value="message", input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`; tweaks=TWEAKS)`;
} }
@ -481,6 +487,120 @@ export function sensitiveSort(a: string, b: string): number {
return a.localeCompare(b); return a.localeCompare(b);
} }
} }
export function groupByFamily(
data: APIDataType,
baseClasses: string,
left: boolean,
flow?: NodeType[],
): groupedObjType[] {
const baseClassesSet = new Set(baseClasses.split("\n"));
let arrOfPossibleInputs: Array<{
category: string;
nodes: nodeGroupedObjType[];
full: boolean;
display_name?: string;
}> = [];
let arrOfPossibleOutputs: Array<{
category: string;
nodes: nodeGroupedObjType[];
full: boolean;
display_name?: string;
}> = [];
let checkedNodes = new Map();
const excludeTypes = new Set(["bool", "float", "code", "file", "int"]);
const checkBaseClass = (template: TemplateVariableType) => {
return (
template.type &&
template.show &&
((!excludeTypes.has(template.type) &&
baseClassesSet.has(template.type)) ||
(template.input_types &&
template.input_types.some((inputType) =>
baseClassesSet.has(inputType),
)))
);
};
if (flow) {
// se existir o flow
for (const node of flow) {
// para cada node do flow
if (node!.data!.node!.flow || !node!.data!.node!.template) break; // não faz nada se o node for um group
const nodeData = node.data;
const foundNode = checkedNodes.get(nodeData.type); // verifica se o tipo do node já foi checado
checkedNodes.set(nodeData.type, {
hasBaseClassInTemplate:
foundNode?.hasBaseClassInTemplate ||
Object.values(nodeData.node!.template).some(checkBaseClass),
hasBaseClassInBaseClasses:
foundNode?.hasBaseClassInBaseClasses ||
nodeData.node!.base_classes.some((baseClass) =>
baseClassesSet.has(baseClass),
), //seta como anterior ou verifica se o node tem base class
displayName: nodeData.node?.display_name,
});
}
}
for (const [d, nodes] of Object.entries(data)) {
let tempInputs: nodeGroupedObjType[] = [],
tempOutputs: nodeGroupedObjType[] = [];
for (const [n, node] of Object.entries(nodes!)) {
let foundNode = checkedNodes.get(n);
if (!foundNode) {
foundNode = {
hasBaseClassInTemplate: Object.values(node!.template).some(
checkBaseClass,
),
hasBaseClassInBaseClasses: node!.base_classes.some((baseClass) =>
baseClassesSet.has(baseClass),
),
displayName: node?.display_name,
};
}
if (foundNode.hasBaseClassInTemplate)
tempInputs.push({ node: n, displayName: foundNode.displayName });
if (foundNode.hasBaseClassInBaseClasses)
tempOutputs.push({ node: n, displayName: foundNode.displayName });
}
const totalNodes = Object.keys(nodes!).length;
if (tempInputs.length)
arrOfPossibleInputs.push({
category: d,
nodes: tempInputs,
full: tempInputs.length === totalNodes,
});
if (tempOutputs.length)
arrOfPossibleOutputs.push({
category: d,
nodes: tempOutputs,
full: tempOutputs.length === totalNodes,
});
}
return left
? arrOfPossibleOutputs.map((output) => ({
family: output.category,
type: output.full
? ""
: output.nodes.map((item) => item.node).join(", "),
display_name: "",
}))
: arrOfPossibleInputs.map((input) => ({
family: input.category,
type: input.full ? "" : input.nodes.map((item) => item.node).join(", "),
display_name: input.nodes.map((item) => item.displayName).join(", "),
}));
}
// this function is used to get the set of keys from an object // this function is used to get the set of keys from an object
export function getSetFromObject(obj: object, key?: string): Set<string> { export function getSetFromObject(obj: object, key?: string): Set<string> {
const set = new Set<string>(); const set = new Set<string>();

View file

@ -4,6 +4,14 @@ test("shoud delete a flow", async ({ page }) => {
await page.goto("/"); await page.goto("/");
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
await page.getByText("Store").nth(0).click(); await page.getByText("Store").nth(0).click();
await page.getByText("API Key", { exact: true }).click();
await page
.getByPlaceholder("Insert your API Key", { exact: true })
.fill(process.env.STORE_API_KEY ?? "");
await page.getByText("Save").last().click();
await page.waitForTimeout(8000);
await page.getByTestId("install-Website Content QA").click(); await page.getByTestId("install-Website Content QA").click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
await page.getByText("My Collection").nth(0).click(); await page.getByText("My Collection").nth(0).click();
@ -16,7 +24,9 @@ test("shoud delete a flow", async ({ page }) => {
await page.getByTestId("icon-Trash2").first().click(); await page.getByTestId("icon-Trash2").first().click();
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
}); });
await page.getByText("Confirm deletion of component?").isVisible(); await page
.getByText("Are you sure you want to delete the selected component?")
.isVisible();
await page.getByText("Delete").nth(1).click(); await page.getByText("Delete").nth(1).click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
await page.getByText("Successfully").first().isVisible(); await page.getByText("Successfully").first().isVisible();
@ -39,7 +49,9 @@ test("shoud delete a component", async ({ page }) => {
await page.getByTestId("icon-Trash2").first().click(); await page.getByTestId("icon-Trash2").first().click();
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
}); });
await page.getByText("Confirm deletion of component?").isVisible(); await page
.getByText("Are you sure you want to delete the selected component?")
.isVisible();
await page.getByText("Delete").nth(1).click(); await page.getByText("Delete").nth(1).click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
await page.getByText("Successfully").first().isVisible(); await page.getByText("Successfully").first().isVisible();

View file

@ -15,7 +15,7 @@ test("NestedComponent", async ({ page }) => {
} }
while (modalCount === 0) { while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click(); await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count(); modalCount = await page.getByTestId("modal-title")?.count();
} }
@ -41,7 +41,7 @@ test("NestedComponent", async ({ page }) => {
await page.locator('//*[@id="showpool_threads"]').click(); await page.locator('//*[@id="showpool_threads"]').click();
expect( expect(
await page.locator('//*[@id="showpool_threads"]').isChecked() await page.locator('//*[@id="showpool_threads"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
//showtext_key //showtext_key
@ -53,182 +53,140 @@ test("NestedComponent", async ({ page }) => {
await page.locator('//*[@id="showindex_name"]').click(); await page.locator('//*[@id="showindex_name"]').click();
expect( expect(
await page.locator('//*[@id="showindex_name"]').isChecked() await page.locator('//*[@id="showindex_name"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
// shownamespace // shownamespace
await page.locator('//*[@id="shownamespace"]').click(); await page.locator('//*[@id="shownamespace"]').click();
expect( expect(
await page.locator('//*[@id="shownamespace"]').isChecked() await page.locator('//*[@id="shownamespace"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
// showpinecone_api_key // showpinecone_api_key
await page.locator('//*[@id="showpinecone_api_key"]').click(); await page.locator('//*[@id="showpinecone_api_key"]').click();
expect( expect(
await page.locator('//*[@id="showpinecone_api_key"]').isChecked() await page.locator('//*[@id="showpinecone_api_key"]').isChecked(),
).toBeFalsy();
// showpinecone_env
await page.locator('//*[@id="showpinecone_env"]').click();
expect(
await page.locator('//*[@id="showpinecone_env"]').isChecked()
).toBeFalsy(); ).toBeFalsy();
// showindex_name // showindex_name
await page.locator('//*[@id="showindex_name"]').click(); await page.locator('//*[@id="showindex_name"]').click();
expect( expect(
await page.locator('//*[@id="showindex_name"]').isChecked() await page.locator('//*[@id="showindex_name"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
// shownamespace // shownamespace
await page.locator('//*[@id="shownamespace"]').click(); await page.locator('//*[@id="shownamespace"]').click();
expect( expect(
await page.locator('//*[@id="shownamespace"]').isChecked() await page.locator('//*[@id="shownamespace"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
// showpinecone_api_key // showpinecone_api_key
await page.locator('//*[@id="showpinecone_api_key"]').click(); await page.locator('//*[@id="showpinecone_api_key"]').click();
expect( expect(
await page.locator('//*[@id="showpinecone_api_key"]').isChecked() await page.locator('//*[@id="showpinecone_api_key"]').isChecked(),
).toBeTruthy();
// showpinecone_env
await page.locator('//*[@id="showpinecone_env"]').click();
expect(
await page.locator('//*[@id="showpinecone_env"]').isChecked()
).toBeTruthy(); ).toBeTruthy();
// showindex_name // showindex_name
await page.locator('//*[@id="showindex_name"]').click(); await page.locator('//*[@id="showindex_name"]').click();
expect( expect(
await page.locator('//*[@id="showindex_name"]').isChecked() await page.locator('//*[@id="showindex_name"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
// shownamespace // shownamespace
await page.locator('//*[@id="shownamespace"]').click(); await page.locator('//*[@id="shownamespace"]').click();
expect( expect(
await page.locator('//*[@id="shownamespace"]').isChecked() await page.locator('//*[@id="shownamespace"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
// showpinecone_api_key // showpinecone_api_key
await page.locator('//*[@id="showpinecone_api_key"]').click(); await page.locator('//*[@id="showpinecone_api_key"]').click();
expect( expect(
await page.locator('//*[@id="showpinecone_api_key"]').isChecked() await page.locator('//*[@id="showpinecone_api_key"]').isChecked(),
).toBeFalsy();
// showpinecone_env
await page.locator('//*[@id="showpinecone_env"]').click();
expect(
await page.locator('//*[@id="showpinecone_env"]').isChecked()
).toBeFalsy(); ).toBeFalsy();
// showindex_name // showindex_name
await page.locator('//*[@id="showindex_name"]').click(); await page.locator('//*[@id="showindex_name"]').click();
expect( expect(
await page.locator('//*[@id="showindex_name"]').isChecked() await page.locator('//*[@id="showindex_name"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
// shownamespace // shownamespace
await page.locator('//*[@id="shownamespace"]').click(); await page.locator('//*[@id="shownamespace"]').click();
expect( expect(
await page.locator('//*[@id="shownamespace"]').isChecked() await page.locator('//*[@id="shownamespace"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
// showpinecone_api_key // showpinecone_api_key
await page.locator('//*[@id="showpinecone_api_key"]').click(); await page.locator('//*[@id="showpinecone_api_key"]').click();
expect( expect(
await page.locator('//*[@id="showpinecone_api_key"]').isChecked() await page.locator('//*[@id="showpinecone_api_key"]').isChecked(),
).toBeTruthy();
// showpinecone_env
await page.locator('//*[@id="showpinecone_env"]').click();
expect(
await page.locator('//*[@id="showpinecone_env"]').isChecked()
).toBeTruthy(); ).toBeTruthy();
// showindex_name // showindex_name
await page.locator('//*[@id="showindex_name"]').click(); await page.locator('//*[@id="showindex_name"]').click();
expect( expect(
await page.locator('//*[@id="showindex_name"]').isChecked() await page.locator('//*[@id="showindex_name"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
// shownamespace // shownamespace
await page.locator('//*[@id="shownamespace"]').click(); await page.locator('//*[@id="shownamespace"]').click();
expect( expect(
await page.locator('//*[@id="shownamespace"]').isChecked() await page.locator('//*[@id="shownamespace"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
// showpinecone_api_key // showpinecone_api_key
await page.locator('//*[@id="showpinecone_api_key"]').click(); await page.locator('//*[@id="showpinecone_api_key"]').click();
expect( expect(
await page.locator('//*[@id="showpinecone_api_key"]').isChecked() await page.locator('//*[@id="showpinecone_api_key"]').isChecked(),
).toBeFalsy();
// showpinecone_env
await page.locator('//*[@id="showpinecone_env"]').click();
expect(
await page.locator('//*[@id="showpinecone_env"]').isChecked()
).toBeFalsy(); ).toBeFalsy();
// showindex_name // showindex_name
await page.locator('//*[@id="showindex_name"]').click(); await page.locator('//*[@id="showindex_name"]').click();
expect( expect(
await page.locator('//*[@id="showindex_name"]').isChecked() await page.locator('//*[@id="showindex_name"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
// shownamespace // shownamespace
await page.locator('//*[@id="shownamespace"]').click(); await page.locator('//*[@id="shownamespace"]').click();
expect( expect(
await page.locator('//*[@id="shownamespace"]').isChecked() await page.locator('//*[@id="shownamespace"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
// showpinecone_api_key // showpinecone_api_key
await page.locator('//*[@id="showpinecone_api_key"]').click(); await page.locator('//*[@id="showpinecone_api_key"]').click();
expect( expect(
await page.locator('//*[@id="showpinecone_api_key"]').isChecked() await page.locator('//*[@id="showpinecone_api_key"]').isChecked(),
).toBeTruthy();
// showpinecone_env
await page.locator('//*[@id="showpinecone_env"]').click();
expect(
await page.locator('//*[@id="showpinecone_env"]').isChecked()
).toBeTruthy(); ).toBeTruthy();
//showpool_threads //showpool_threads
await page.locator('//*[@id="showpool_threads"]').click(); await page.locator('//*[@id="showpool_threads"]').click();
expect( expect(
await page.locator('//*[@id="showpool_threads"]').isChecked() await page.locator('//*[@id="showpool_threads"]').isChecked(),
).toBeFalsy(); ).toBeFalsy();
//showtext_key //showtext_key
await page.locator('//*[@id="showtext_key"]').click(); await page.locator('//*[@id="showtext_key"]').click();
expect( expect(
await page.locator('//*[@id="showtext_key"]').isChecked() await page.locator('//*[@id="showtext_key"]').isChecked(),
).toBeTruthy(); ).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click(); await page.locator('//*[@id="saveChangesBtn"]').click();

View file

@ -16,7 +16,7 @@ test.describe("save component tests", () => {
} }
while (modalCount === 0) { while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click(); await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count(); modalCount = await page.getByTestId("modal-title")?.count();
} }
@ -27,7 +27,7 @@ test.describe("save component tests", () => {
// Read your file into a buffer. // Read your file into a buffer.
const jsonContent = readFileSync( const jsonContent = readFileSync(
"tests/end-to-end/assets/flow_group_test.json", "tests/end-to-end/assets/flow_group_test.json",
"utf-8" "utf-8",
); );
// Create the DataTransfer and File // Create the DataTransfer and File
@ -49,7 +49,7 @@ test.describe("save component tests", () => {
"drop", "drop",
{ {
dataTransfer, dataTransfer,
} },
); );
const genericNoda = page.getByTestId("div-generic-node"); const genericNoda = page.getByTestId("div-generic-node");
@ -61,9 +61,6 @@ test.describe("save component tests", () => {
.locator('//*[@id="react-flow-id"]/div[1]/div[2]/button[3]') .locator('//*[@id="react-flow-id"]/div[1]/div[2]/button[3]')
.click(); .click();
await page.getByTestId("title-PythonFunctionTool").click({
modifiers: ["Control"],
});
await page.getByTestId("title-ChatOpenAI").click({ await page.getByTestId("title-ChatOpenAI").click({
modifiers: ["Control"], modifiers: ["Control"],
}); });

View file

@ -144,7 +144,7 @@ test("should add API-KEY", async ({ page }) => {
await page.getByTestId("api-key-button-store").click(); await page.getByTestId("api-key-button-store").click();
await page await page
.getByPlaceholder("Insert your API Key") .getByPlaceholder("Insert your API Key")
.fill("x1fOKU0v2e5zL5d-BZW6CxZBZvoyuFgF"); .fill(process.env.STORE_API_KEY ?? "");
await page.getByTestId("api-key-save-button-store").click(); await page.getByTestId("api-key-save-button-store").click();
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
@ -165,7 +165,7 @@ test("should like and add components and flows", async ({ page }) => {
await page await page
.getByPlaceholder("Insert your API Key") .getByPlaceholder("Insert your API Key")
.fill("x1fOKU0v2e5zL5d-BZW6CxZBZvoyuFgF"); .fill(process.env.STORE_API_KEY ?? "");
await page.getByTestId("api-key-save-button-store").click(); await page.getByTestId("api-key-save-button-store").click();
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
@ -174,6 +174,8 @@ test("should like and add components and flows", async ({ page }) => {
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
await page.getByText("API Key Error").isHidden(); await page.getByText("API Key Error").isHidden();
await page.waitForTimeout(5000);
const likedValue = await page const likedValue = await page
.getByTestId("likes-Website Content QA") .getByTestId("likes-Website Content QA")
.innerText(); .innerText();
@ -214,7 +216,7 @@ test("should like and add components and flows", async ({ page }) => {
await page.getByText("My Collection").click(); await page.getByText("My Collection").click();
await page.getByText("Website Content QA").first().isVisible(); await page.getByText("Website Content QA").first().isVisible();
await page.getByTestId("sidebar-nav-Components").click(); await page.getByText("Components").first().click();
await page.getByText("Basic RAG").first().isVisible(); await page.getByText("Basic RAG").first().isVisible();
}); });
@ -233,7 +235,7 @@ test("should share component with share button", async ({ page }) => {
} }
while (modalCount === 0) { while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click(); await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count(); modalCount = await page.getByTestId("modal-title")?.count();
} }
@ -255,7 +257,7 @@ test("should share component with share button", async ({ page }) => {
await page.getByText("Set workflow status to public").isVisible(); await page.getByText("Set workflow status to public").isVisible();
await page await page
.getByText( .getByText(
"Attention: API keys in specified fields are automatically removed upon sharing." "Attention: API keys in specified fields are automatically removed upon sharing.",
) )
.isVisible(); .isVisible();
await page.getByText("Export").first().isVisible(); await page.getByText("Export").first().isVisible();

View file

@ -13,7 +13,7 @@ test("curl_api_generation", async ({ page, context }) => {
} }
while (modalCount === 0) { while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click(); await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count(); modalCount = await page.getByTestId("modal-title")?.count();
} }
@ -75,7 +75,7 @@ test("check if tweaks are updating when someothing on the flow changes", async (
} }
while (modalCount === 0) { while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click(); await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count(); modalCount = await page.getByTestId("modal-title")?.count();
} }

View file

@ -10,7 +10,6 @@ import orjson
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from httpx import AsyncClient from httpx import AsyncClient
from sqlmodel import Session, SQLModel, create_engine, select
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
from langflow.initial_setup.setup import STARTER_FOLDER_NAME from langflow.initial_setup.setup import STARTER_FOLDER_NAME
from langflow.services.auth.utils import get_password_hash from langflow.services.auth.utils import get_password_hash
@ -19,6 +18,7 @@ from langflow.services.database.models.flow.model import Flow, FlowCreate
from langflow.services.database.models.user.model import User, UserCreate from langflow.services.database.models.user.model import User, UserCreate
from langflow.services.database.utils import session_getter from langflow.services.database.utils import session_getter
from langflow.services.deps import get_db_service from langflow.services.deps import get_db_service
from sqlmodel import Session, SQLModel, create_engine, select
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
from typer.testing import CliRunner from typer.testing import CliRunner
@ -26,7 +26,8 @@ if TYPE_CHECKING:
from langflow.services.database.service import DatabaseService from langflow.services.database.service import DatabaseService
def pytest_configure(): def pytest_configure(config):
config.addinivalue_line("markers", "noclient: don't create a client for this test")
data_path = Path(__file__).parent.absolute() / "data" data_path = Path(__file__).parent.absolute() / "data"
pytest.BASIC_EXAMPLE_PATH = data_path / "basic_example.json" pytest.BASIC_EXAMPLE_PATH = data_path / "basic_example.json"

View file

@ -407,7 +407,7 @@ def test_update_source_handle():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_pickle_graph(json_vector_store): async def test_pickle_graph():
starter_projects = load_starter_projects() starter_projects = load_starter_projects()
data = starter_projects[0][1]["data"] data = starter_projects[0][1]["data"]
graph = Graph.from_payload(data) graph = Graph.from_payload(data)

View file

@ -3,8 +3,7 @@ from typing import Dict, List, Optional
import pytest import pytest
from langflow.interface.utils import build_template_from_class from langflow.interface.utils import build_template_from_class
from langflow.utils.constants import CHAT_OPENAI_MODELS, OPENAI_MODELS from langflow.utils.util import build_template_from_function, get_base_classes, get_default_factory
from langflow.utils.util import build_template_from_function, format_dict, get_base_classes, get_default_factory
from pydantic import BaseModel from pydantic import BaseModel
@ -88,171 +87,6 @@ def test_build_template_from_class():
build_template_from_class("InvalidClass", type_to_cls_dict) build_template_from_class("InvalidClass", type_to_cls_dict)
# Test format_dict
def test_format_dict():
# Test 1: Optional type removal
input_dict = {
"field1": {"type": "Optional[str]", "required": False},
}
expected_output = {
"field1": {
"type": "str",
"required": False,
"list": False,
"show": False,
"password": False,
"multiline": False,
},
}
assert format_dict(input_dict) == expected_output
# Test 2: List type processing
input_dict = {
"field1": {"type": "List[str]", "required": False},
}
expected_output = {
"field1": {
"type": "str",
"required": False,
"list": True,
"show": False,
"password": False,
"multiline": False,
},
}
assert format_dict(input_dict) == expected_output
# Test 3: Mapping type replacement
input_dict = {
"field1": {"type": "Mapping[str, int]", "required": False},
}
expected_output = {
"field1": {
"type": "dict[str, int]", # Mapping type is replaced with dict which is replaced with code
"required": False,
"list": False,
"show": False,
"password": False,
"multiline": False,
},
}
assert format_dict(input_dict) == expected_output
# Test 4: Replace default value with actual value
input_dict = {
"field1": {"type": "str", "required": False, "default": "test"},
}
expected_output = {
"field1": {
"type": "str",
"required": False,
"list": False,
"show": False,
"password": False,
"multiline": False,
"value": "test",
},
}
assert format_dict(input_dict) == expected_output
# Test 5: Add password field
input_dict = {
"field1": {"type": "str", "required": False},
"api_key": {"type": "str", "required": False},
}
expected_output = {
"field1": {
"type": "str",
"required": False,
"list": False,
"show": False,
"password": False,
"multiline": False,
},
"api_key": {
"type": "str",
"required": False,
"list": False,
"show": True,
"password": True,
"multiline": False,
},
}
assert format_dict(input_dict) == expected_output
# Test 6: Add multiline
input_dict = {
"field1": {"type": "str", "required": False},
"prefix": {"type": "str", "required": False},
}
expected_output = {
"field1": {
"type": "str",
"required": False,
"list": False,
"show": False,
"password": False,
"multiline": False,
},
"prefix": {
"type": "str",
"required": False,
"list": False,
"show": True,
"password": False,
"multiline": True,
},
}
assert format_dict(input_dict) == expected_output
# Test 7: Check class name-specific cases (OpenAI, ChatOpenAI)
input_dict = {
"model_name": {"type": "str", "required": False},
}
expected_output_openai = {
"model_name": {
"type": "str",
"required": False,
"list": True,
"show": True,
"password": False,
"multiline": False,
"options": OPENAI_MODELS,
"value": "text-davinci-003",
},
}
expected_output_openai_chat = {
"model_name": {
"type": "str",
"required": False,
"list": True,
"show": True,
"password": False,
"multiline": False,
"options": CHAT_OPENAI_MODELS,
"value": "gpt-4-turbo-preview",
},
}
assert format_dict(input_dict, "OpenAI") == expected_output_openai
assert format_dict(input_dict, "ChatOpenAI") == expected_output_openai_chat
# Test 8: Replace dict type with str
input_dict = {
"field1": {"type": "Dict[str, int]", "required": False},
}
expected_output = {
"field1": {
"type": "Dict[str, int]",
"required": False,
"list": False,
"show": False,
"password": False,
"multiline": False,
},
}
assert format_dict(input_dict) == expected_output
# Test get_base_classes # Test get_base_classes
def test_get_base_classes(): def test_get_base_classes():
base_classes_parent = get_base_classes(Parent) base_classes_parent = get_base_classes(Parent)