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 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
# Postgres example: LANGFLOW_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/langflow
# SQLite example:
@ -56,7 +69,6 @@ LANGFLOW_REMOVE_API_KEYS=
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
LANGFLOW_CACHE_TYPE=
# 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
# 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
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 Render

View file

@ -10,7 +10,7 @@
# PYTHON-BASE
# Sets up all our shared environment variables
################################
FROM python:3.10-slim as python-base
FROM python:3.12-slim as python-base
# python
ENV PYTHONUNBUFFERED=1 \
@ -47,7 +47,7 @@ ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
# Used to build deps + create our virtual environment
################################
FROM python-base as builder-base
RUN
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# deps for installing poetry
@ -55,7 +55,12 @@ RUN apt-get update \
# deps for building python deps
build-essential \
# 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
@ -70,15 +75,12 @@ RUN --mount=type=cache,target=/root/.cache \
RUN python -m pip install requests && cd ./scripts && python update_dependencies.py
RUN $POETRY_HOME/bin/poetry lock
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
RUN useradd -m -u 1000 user
COPY --from=builder-base /app/dist/*.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"]
CMD ["--host", "0.0.0.0", "--port", "7860"]

View file

@ -10,7 +10,7 @@
# PYTHON-BASE
# Sets up all our shared environment variables
################################
FROM python:3.10-slim as python-base
FROM python:3.12-slim as python-base
# python
ENV PYTHONUNBUFFERED=1 \
@ -55,6 +55,8 @@ RUN apt-get update \
build-essential \
# npm
npm \
# gcc
gcc \
&& apt-get clean \
&& 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 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
RUN useradd -m -u 1000 user
COPY --from=builder-base /app/src/backend/base/dist/*.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"]
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",
label: "What's New?",
collapsed: false,
items: [
"whats-new/a-new-chapter-langflow"
],
items: ["whats-new/a-new-chapter-langflow"],
},
{
type: "category",
@ -17,7 +15,7 @@ module.exports = {
"getting-started/install-langflow",
"getting-started/quickstart",
"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/document-qa",
"starter-projects/memory-chatbot",
"starter-projects/vector-store-rag"
"starter-projects/vector-store-rag",
],
},
{
@ -40,10 +38,12 @@ module.exports = {
"administration/login",
"administration/api",
"administration/cli",
"administration/playground",
"administration/global-env",
"administration/components",
"administration/collection",
"administration/prompt-customization",
"administration/langfuse_integration"
"administration/langfuse_integration",
],
},
{
@ -58,7 +58,7 @@ module.exports = {
"components/helpers",
"components/vector-stores",
"components/embeddings",
"components/custom"
"components/custom",
],
},
{
@ -74,7 +74,7 @@ module.exports = {
"components/retrievers",
"components/text-splitters",
"components/toolkits",
"components/tools"
"components/tools",
],
},
{
@ -88,7 +88,7 @@ module.exports = {
"examples/csv-loader",
"examples/searchapi-tool",
"examples/serp-api-tool",
"examples/python-function"
"examples/python-function",
],
},
{
@ -101,8 +101,8 @@ module.exports = {
"migration/inputs-and-outputs",
"migration/text-and-record",
"migration/compatibility",
"migration/global-variables"
]
"migration/global-variables",
],
},
{
type: "category",
@ -111,16 +111,14 @@ module.exports = {
items: [
"tutorials/chatprompttemplate_guide",
"tutorials/loading_document",
"tutorials/rag-with-astradb"
"tutorials/rag-with-astradb",
],
},
{
type: "category",
label: "Deployment",
collapsed: true,
items: [
"deployment/gcp-deployment"
],
items: ["deployment/gcp-deployment"],
},
{
type: "category",
@ -130,7 +128,7 @@ module.exports = {
"contributing/how-contribute",
"contributing/github-issues",
"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]
name = "langflow"
version = "1.0.0a33"
version = "1.0.0a34"
description = "A Python package with a built-in web application"
authors = ["Langflow <contact@langflow.org>"]
maintainers = [

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -2,9 +2,9 @@ import platform
import socket
import sys
import time
import warnings
from pathlib import Path
from typing import Optional
import warnings
import click
import httpx
@ -443,6 +443,41 @@ def superuser(
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()
def migration(
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_name: Optional[str] = None,
) -> list[Record]:
records = store_message(
message,
session_id=session_id,

View file

@ -1,7 +1,5 @@
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.schema.schema import Record

View file

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

View file

@ -3,11 +3,12 @@ from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record
from langflow.field_typing import Text
class PassComponent(CustomComponent):
display_name = "Pass"
description = "A pass-through component that forwards the second input while ignoring the first, used for controlling workflow direction."
field_order = ["ignored_input", "forwarded_input"]
def build_config(self) -> dict:
return {
"ignored_input": {
@ -19,7 +20,7 @@ class PassComponent(CustomComponent):
"display_name": "Input",
"info": "This input is forwarded by the component.",
"input_types": ["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,
message: str = "",
) -> List[Record]:
store_message(
sender=sender,
sender_name=sender_name,

View file

@ -35,7 +35,7 @@ class SubFlowComponent(CustomComponent):
build_config["flow_name"]["options"] = self.get_flow_names()
# Clean up the build config
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]
if field_value is not None and field_name == "flow_name":
try:
@ -85,20 +85,29 @@ class SubFlowComponent(CustomComponent):
"display_name": "Tweaks",
"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
if not messages:
return []
records = []
for message in messages:
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)
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()}
run_outputs: List[Optional[RunOutputs]] = await self.run_flow(
tweaks=tweaks,
@ -112,7 +121,7 @@ class SubFlowComponent(CustomComponent):
if run_output is not None:
for output in run_output.outputs:
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
logger.debug(records)

View file

@ -4,6 +4,7 @@ from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record
from langflow.field_typing import Text
class TextOperatorComponent(CustomComponent):
display_name = "Text Operator"
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": {
"display_name": "Operator",
"info": "The operator to apply for comparing the texts.",
"options": [
"equals",
"not equals",
"contains",
"starts with",
"ends with",
"exists"
],
"options": ["equals", "not equals", "contains", "starts with", "ends with", "exists"],
},
"case_sensitive": {
"display_name": "Case Sensitive",
@ -51,11 +45,8 @@ class TextOperatorComponent(CustomComponent):
case_sensitive: bool = False,
true_output: Optional[Text] = "",
) -> Union[Text, Record]:
if not input_text or not match_text:
raise ValueError(
"Both 'input_text' and 'match_text' must be provided and non-empty."
)
raise ValueError("Both 'input_text' and 'match_text' must be provided and non-empty.")
if not case_sensitive:
input_text = input_text.lower()
@ -82,4 +73,4 @@ class TextOperatorComponent(CustomComponent):
self.status = "Comparison failed, stopping execution."
self.stop()
return output_record
return output_record

View file

@ -242,6 +242,7 @@ class Graph:
outputs: list[str],
stream: bool,
session_id: str,
fallback_to_env_vars: bool,
) -> List[Optional["ResultData"]]:
"""
Runs the graph with the given inputs.
@ -289,7 +290,7 @@ class Graph:
start_component_id = next(
(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()
except Exception as exc:
logger.exception(exc)
@ -315,6 +316,7 @@ class Graph:
outputs: Optional[list[str]] = None,
session_id: Optional[str] = None,
stream: bool = False,
fallback_to_env_vars: bool = False,
) -> List[RunOutputs]:
"""
Run the graph with the given inputs and return the outputs.
@ -340,6 +342,7 @@ class Graph:
outputs=outputs,
session_id=session_id,
stream=stream,
fallback_to_env_vars=fallback_to_env_vars,
)
try:
@ -362,6 +365,7 @@ class Graph:
outputs: Optional[list[str]] = None,
session_id: Optional[str] = None,
stream: bool = False,
fallback_to_env_vars: bool = False,
) -> List[RunOutputs]:
"""
Runs the graph with the given inputs.
@ -403,6 +407,7 @@ class Graph:
outputs=outputs or [],
stream=stream,
session_id=session_id or "",
fallback_to_env_vars=fallback_to_env_vars,
)
run_output_object = RunOutputs(inputs=run_inputs, outputs=run_outputs)
logger.debug(f"Run outputs: {run_output_object}")
@ -468,9 +473,9 @@ class Graph:
"""Marks a branch of the graph."""
if visited is None:
visited = set()
visited.add(vertex_id)
if vertex_id in visited:
return
visited.add(vertex_id)
self.mark_vertex(vertex_id, state)
@ -712,6 +717,7 @@ class Graph:
vertex_id: str,
inputs_dict: Optional[Dict[str, str]] = None,
user_id: Optional[str] = None,
fallback_to_env_vars: bool = False,
):
"""
Builds a vertex in the graph.
@ -733,7 +739,7 @@ class Graph:
vertex = self.get_vertex(vertex_id)
try:
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:
params = vertex._built_object_repr()
@ -796,7 +802,7 @@ class Graph:
vertices.append(vertex)
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."""
first_layer = self.sort_vertices(start_component_id=start_component_id)
@ -821,6 +827,7 @@ class Graph:
vertex_id=vertex_id,
user_id=self.user_id,
inputs_dict={},
fallback_to_env_vars=fallback_to_env_vars,
),
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
elif field.get("required"):
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:
val = field.get("value")
if field.get("type") == "code":
@ -390,13 +394,17 @@ class Vertex:
self.params = self._raw_params.copy()
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.
"""
logger.debug(f"Building {self.display_name}")
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._built = True
@ -606,7 +614,7 @@ class Vertex:
if isinstance(self.params[key], list):
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.
"""
@ -615,6 +623,7 @@ class Vertex:
try:
result = await loading.instantiate_class(
user_id=user_id,
fallback_to_env_vars=fallback_to_env_vars,
vertex=self,
)
self._update_built_object_and_artifacts(result)

View file

@ -1,8 +1,8 @@
import inspect
import json
import os
from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type
import orjson
from langchain.agents import agent as agent_module
from langchain.agents.agent import AgentExecutor
@ -35,6 +35,7 @@ if TYPE_CHECKING:
async def instantiate_class(
vertex: "Vertex",
fallback_to_env_vars,
user_id=None,
) -> Any:
"""Instantiate class from module type and key, and params"""
@ -57,7 +58,7 @@ async def instantiate_class(
if not base_type:
raise ValueError("No base type provided for vertex")
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)
return await instantiate_based_on_type(
class_object=class_object,
@ -66,6 +67,7 @@ async def instantiate_class(
params=params,
user_id=user_id,
vertex=vertex,
fallback_to_env_vars=fallback_to_env_vars,
)
@ -93,14 +95,7 @@ def convert_kwargs(params):
return params
async def instantiate_based_on_type(
class_object,
base_type,
node_type,
params,
user_id,
vertex,
):
async def instantiate_based_on_type(class_object, base_type, node_type, params, user_id, vertex, fallback_to_env_vars):
if base_type == "agents":
return instantiate_agent(node_type, class_object, params)
elif base_type == "prompts":
@ -132,33 +127,49 @@ async def instantiate_based_on_type(
elif base_type == "memory":
return instantiate_memory(node_type, class_object, params)
elif 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)
elif base_type == "wrappers":
return instantiate_wrapper(node_type, class_object, params)
else:
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
# and if it is, we will get the value from the custom_component.keys(name)
# and update the params with the value
for field in load_from_db_fields:
if field in params:
try:
key = custom_component.variables(params[field])
params[field] = key if key else params[field]
key = None
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:
logger.error(f"Failed to get value for {field} from custom component. Error: {exc}")
pass
logger.error(f"Failed to get value for {field} from custom component. Setting it to None. Error: {exc}")
params[field] = None
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()
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
custom_component: "CustomComponent" = class_object(
@ -167,7 +178,9 @@ async def instantiate_custom_component(params, user_id, vertex):
vertex=vertex,
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"):
params_copy["retriever"] = params_copy["retriever"].as_retriever()

View file

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

View file

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

View file

@ -82,6 +82,7 @@ def run_flow_from_json(
env_file: Optional[str] = None,
cache: Optional[str] = None,
disable_logs: Optional[bool] = True,
fallback_to_env_vars: bool = False,
) -> List[RunOutputs]:
"""
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.
cache (Optional[str], optional): The cache directory to use. Defaults to None.
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:
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,
output_type=output_type,
output_component=output_component,
fallback_to_env_vars=fallback_to_env_vars,
)
return result

View file

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

View file

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

View file

@ -7,13 +7,12 @@ from typing import Any, List, Optional, Tuple, Type
import orjson
import yaml
from langflow.services.settings.constants import VARIABLES_TO_GET_FROM_ENVIRONMENT
from loguru import logger
from pydantic import field_validator, validator
from pydantic import field_validator
from pydantic.fields import FieldInfo
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.parent.parent / "components")
@ -76,6 +75,10 @@ class Settings(BaseSettings):
# Define the default LANGFLOW_DIR
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
DATABASE_URL: Optional[str] = None
@ -108,12 +111,16 @@ class Settings(BaseSettings):
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
"""Whether to store environment variables as Global Variables in the database."""
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."""
@validator("CONFIG_DIR", pre=True, allow_reuse=True)
@field_validator("CONFIG_DIR", mode="before")
def set_langflow_dir(cls, value):
if not value:
from platformdirs import user_cache_dir
@ -136,8 +143,8 @@ class Settings(BaseSettings):
return str(value)
@validator("DATABASE_URL", pre=True)
def set_database_url(cls, value, values):
@field_validator("DATABASE_URL", mode="before")
def set_database_url(cls, value, info):
if not value:
logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable")
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
@ -148,29 +155,36 @@ class Settings(BaseSettings):
# Originally, we used sqlite:///./langflow.db
# so we need to migrate to the new format
# 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")
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"
db_file_name = "langflow.db"
new_pre_path = f"{values['CONFIG_DIR']}/{pre_db_file_name}"
new_path = f"{values['CONFIG_DIR']}/{db_file_name}"
new_pre_path = f"{database_dir}/{pre_db_file_name}"
new_path = f"{database_dir}/{db_file_name}"
final_path = None
if is_pre_release:
if Path(new_pre_path).exists():
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
logger.debug("Copying existing database to new location")
copy2(new_path, 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")
copy2(f"./{db_file_name}", new_pre_path)
logger.debug(f"Copied existing database to {new_pre_path}")
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
else:
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}")
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 yaml
from loguru import logger
from langflow.services.base import Service
from langflow.services.settings.auth import AuthSettings
from langflow.services.settings.base import Settings
from loguru import logger
class SettingsService(Service):
@ -31,7 +30,7 @@ class SettingsService(Service):
for key in settings_dict:
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}")
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]]
name = "aiohttp"
@ -1137,20 +1137,20 @@ extended-testing = ["faker (>=19.3.1,<20.0.0)", "jinja2 (>=3,<4)", "pandas (>=2.
[[package]]
name = "langchain-text-splitters"
version = "0.0.1"
version = "0.0.2"
description = "LangChain text splitting utilities"
optional = false
python-versions = ">=3.8.1,<4.0"
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langchain_text_splitters-0.0.1-py3-none-any.whl", hash = "sha256:f5b802f873f5ff6a8b9259ff34d53ed989666ef4e1582e6d1adb3b5520e3839a"},
{file = "langchain_text_splitters-0.0.1.tar.gz", hash = "sha256:ac459fa98799f5117ad5425a9330b21961321e30bc19a2a2f9f761ddadd62aa1"},
{file = "langchain_text_splitters-0.0.2-py3-none-any.whl", hash = "sha256:13887f32705862c1e1454213cb7834a63aae57c26fcd80346703a1d09c46168d"},
{file = "langchain_text_splitters-0.0.2.tar.gz", hash = "sha256:ac8927dc0ba08eba702f6961c9ed7df7cead8de19a9f7101ab2b5ea34201b3c1"},
]
[package.dependencies]
langchain-core = ">=0.1.28,<0.2.0"
langchain-core = ">=0.1.28,<0.3"
[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]]
name = "langchainhub"
@ -1169,13 +1169,13 @@ types-requests = ">=2.31.0.2,<3.0.0.0"
[[package]]
name = "langsmith"
version = "0.1.57"
version = "0.1.59"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langsmith-0.1.57-py3-none-any.whl", hash = "sha256:dbd83b0944a2fbea4151f0aa053530d93fcf6784a580621bc60633cb890b57dc"},
{file = "langsmith-0.1.57.tar.gz", hash = "sha256:4682204de19f0218029c2b8445ce2cc3485c8d0df9796b31e2ce4c9051fce365"},
{file = "langsmith-0.1.59-py3-none-any.whl", hash = "sha256:445e3bc1d3baa1e5340cd979907a19483b9763a2ed37b863a01113d406f69345"},
{file = "langsmith-0.1.59.tar.gz", hash = "sha256:e748a89f4dd6aa441349143e49e546c03b5dfb43376a25bfef6a5ca792fe1437"},
]
[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-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_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_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-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_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-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
@ -1935,13 +1939,13 @@ xmp = ["defusedxml"]
[[package]]
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`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"},
{file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"},
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
@ -2180,13 +2184,13 @@ cli = ["click (>=5.0)"]
[[package]]
name = "python-engineio"
version = "4.9.0"
version = "4.9.1"
description = "Engine.IO server and client for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "python-engineio-4.9.0.tar.gz", hash = "sha256:e87459c15638e567711fd156e6f9c4a402668871bed79523f0ecfec744729ec7"},
{file = "python_engineio-4.9.0-py3-none-any.whl", hash = "sha256:979859bff770725b75e60353d7ae53b397e8b517d05ba76733b404a3dcca3e4c"},
{file = "python_engineio-4.9.1-py3-none-any.whl", hash = "sha256:f995e702b21f6b9ebde4e2000cd2ad0112ba0e5116ec8d22fe3515e76ba9dddd"},
{file = "python_engineio-4.9.1.tar.gz", hash = "sha256:7631cf5563086076611e494c643b3fa93dd3a854634b5488be0bba0ef9b99709"},
]
[package.dependencies]

View file

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

View file

@ -1,141 +1,162 @@
import { saveAs } from "file-saver";
import OpenSeadragon from "openseadragon";
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 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 [errorDownloading, setErrordownloading] = useState(false)
const setErrorList = useAlertStore(state => state.setErrorData);
const [errorDownloading, setErrordownloading] = useState(false);
const setErrorList = useAlertStore((state) => state.setErrorData);
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(() => {
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');
zoomInButton!.addEventListener('click', () => viewer.viewport.zoomBy(1.2));
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 () => {
viewer.destroy();
zoomInButton!.removeEventListener('click', () => viewer.viewport.zoomBy(1.2));
zoomOutButton!.removeEventListener('click', () => viewer.viewport.zoomBy(0.8));
homeButton!.removeEventListener('click', () => viewer.viewport.goHome());
fullPageButton!.removeEventListener('click', () => viewer.setFullScreen(true));
};
}
} catch (error) {
console.error('Error initializing OpenSeadragon:', error);
}
}, [image]);
zoomInButton!.addEventListener("click", () =>
viewer.viewport.zoomBy(1.2),
);
zoomOutButton!.addEventListener("click", () =>
viewer.viewport.zoomBy(0.8),
);
homeButton!.addEventListener("click", () => viewer.viewport.goHome());
fullPageButton!.addEventListener("click", () =>
viewer.setFullScreen(true),
);
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)
});
// Optionally, you can set additional viewer options here
// Cleanup function
return () => {
viewer.destroy();
zoomInButton!.removeEventListener("click", () =>
viewer.viewport.zoomBy(1.2),
);
zoomOutButton!.removeEventListener("click", () =>
viewer.viewport.zoomBy(0.8),
);
homeButton!.removeEventListener("click", () =>
viewer.viewport.goHome(),
);
fullPageButton!.removeEventListener("click", () =>
viewer.setFullScreen(true),
);
};
}
} catch (error) {
console.error("Error initializing OpenSeadragon:", error);
}
}, [image]);
return (
image === "" ? (
<div className="w-full h-full bg-muted rounded-md flex align-center justify-center flex-col gap-5 border border-border">
<div className="flex gap-2 align-center justify-center ">
<ForwardedIconComponent
name="Image"
/>
{IMGViewErrorTitle}
</div>
<div className="flex align-center justify-center">
<div className="langflow-chat-desc flex align-center justify-center">
<div className="langflow-chat-desc-span">
{IMGViewErrorMSG}
</div>
</div>
</div>
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);
});
}
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 className="w-full flex align-center justify-center my-2 mb-4">
<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
name="ZoomIn"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<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>
<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={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</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 useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { FlowType } from "../../types/flow";
import { storeComponent } from "../../types/store";
import cloneFLowWithParent, {
getInputsAndOutputs,
@ -22,7 +23,6 @@ import {
CardTitle,
} from "../ui/card";
import Loading from "../ui/loading";
import { FlowType } from "../../types/flow";
export default function CollectionCardComponent({
data,

View file

@ -26,6 +26,8 @@ import {
TabsTrigger,
} from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import ExportModal from "../../modals/exportModal";
import { Case } from "../../shared/components/caseComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import { codeTabsPropsType } from "../../types/components";
@ -43,7 +45,6 @@ import KeypairListComponent from "../keypairListComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import ExportModal from "../../modals/exportModal";
export default function CodeTabsComponent({
flow,
@ -54,6 +55,7 @@ export default function CodeTabsComponent({
tweaks,
setActiveTweaks,
activeTweaks,
allowExport = false,
}: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
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 fileExtension = tabs[activeTab].language || ".txt";
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={
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
{tweaks && (
<div
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")
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
htmlFor="tweaks-switch"
>
Tweaks
</Label>
</div>
<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
<Switch
style={{
transform: `scaleX(${0.7}) scaleY(${0.7})`,
}}
id="tweaks-switch"
onCheckedChange={setActiveTweaks}
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>
</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 && (
<>
@ -276,107 +286,71 @@ export default function CodeTabsComponent({
</TableCell>
<TableCell className="p-0 text-xs text-foreground">
<div className="m-auto w-[250px]">
{node.data.node.template[
templateField
].type === "str" &&
!node.data.node.template[
templateField
].options ? (
<div className="mx-auto">
{node.data.node.template[
<Case
condition={
type(node, templateField) ===
"str" &&
!node.data.node.template[
templateField
]?.list ? (
<InputListComponent
componentName={
templateField
}
editNode={true}
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[
].options
}
>
<Case
condition={
node.data.node.template[
templateField
].multiline ? (
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node
.template[
]?.list
}
>
<InputListComponent
componentName={templateField}
editNode={true}
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].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
],
);
}}
/>
</div>
) : (
<InputComponent
editNode={true}
disabled={false}
password={
].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
].password ?? false
}
],
);
}}
/>
</Case>
<Case
condition={
node.data.node.template[
templateField
].multiline
}
>
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node.template[
templateField
@ -410,11 +384,68 @@ export default function CodeTabsComponent({
);
}}
/>
)}
</div>
) : node.data.node.template[
templateField
].type === "bool" ? (
</div>
</Case>
<Case
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">
{" "}
<ToggleShadComponent
@ -446,9 +477,14 @@ export default function CodeTabsComponent({
disabled={false}
/>
</div>
) : node.data.node.template[
templateField
].type === "file" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"file"
}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
@ -473,9 +509,14 @@ export default function CodeTabsComponent({
}}
></InputFileComponent>
</div>
) : node.data.node.template[
templateField
].type === "float" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"float"
}
>
<div className="mx-auto">
<FloatComponent
disabled={false}
@ -518,12 +559,17 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "str" &&
node.data.node.template[
templateField
].options ? (
</Case>
<Case
condition={
type(node, templateField) ===
"str" &&
node.data.node.template[
templateField
].options
}
>
<div className="mx-auto">
<Dropdown
editNode={true}
@ -565,9 +611,14 @@ export default function CodeTabsComponent({
}
></Dropdown>
</div>
) : node.data.node.template[
templateField
].type === "int" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"int"
}
>
<div className="mx-auto">
<IntComponent
disabled={false}
@ -610,9 +661,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "prompt" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"prompt"
}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={true}
@ -651,9 +707,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "code" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"code"
}
>
<div className="mx-auto">
<CodeAreaComponent
disabled={false}
@ -692,9 +753,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "dict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"dict"
}
>
<div className="mx-auto overflow-auto custom-scroll">
<KeypairListComponent
disabled={false}
@ -712,6 +778,10 @@ export default function CodeTabsComponent({
.template[
templateField
].value,
type(
node,
templateField,
),
)
}
duplicateKey={
@ -755,9 +825,14 @@ export default function CodeTabsComponent({
}
/>
</div>
) : node.data.node.template[
templateField
].type === "NestedDict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"NestedDict"
}
>
<div className="mx-auto">
<DictComponent
disabled={false}
@ -765,7 +840,7 @@ export default function CodeTabsComponent({
value={
node.data.node!.template[
templateField
].value.toString() === "{}"
].value?.toString() === "{}"
? {
// yourkey: "value",
}
@ -795,13 +870,16 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</Case>
<Case
condition={
type(node, templateField) ===
"Any"
}
>
<>-</>
</Case>
</div>
</TableCell>
</TableRow>

View file

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

View file

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

View file

@ -169,6 +169,7 @@ export default function InputComponent({
className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
avoidCollisions={false}
align="center"
>
<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";
export default function Error(): JSX.Element {
return (
<div className="flex flex-col items-center justify-center h-full w-full bg-muted">
<div className="chat-alert-box">
<span className="flex gap-2">
<IconComponent name="FileX2" />
<span className="langflow-chat-span">{PDFLoadErrorTitle}</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">
{PDFCheckFlow}{" "}
</span>
</div>
</div>
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
<div className="chat-alert-box">
<span className="flex gap-2">
<IconComponent name="FileX2" />
<span className="langflow-chat-span">{PDFLoadErrorTitle}</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">{PDFCheckFlow} </span>
</div>
);
}
</div>
</div>
);
}

View file

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

View file

@ -1,5 +1,5 @@
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 CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
@ -18,20 +18,15 @@ import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { Case } from "../../../../shared/components/caseComponent";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useTypesStore } from "../../../../stores/typesStore";
import {
APIClassType,
ResponseErrorDetailAPI,
ResponseErrorTypeAPI,
} from "../../../../types/api";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import {
debouncedHandleUpdateValues,
@ -44,13 +39,13 @@ import {
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils";
import groupByFamily from "./utils/group-by-family";
import { nodeColors } from "../../../../utils/styleUtils";
import { classNames, groupByFamily } from "../../../../utils/utils";
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
import TooltipRenderComponent from "../tooltipRenderComponent";
export default function ParameterComponent({
left,
@ -76,175 +71,69 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [isLoading, setIsLoading] = useState(false);
const updateNodeInternals = useUpdateNodeInternals();
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
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 =
edges.some(
(edge) =>
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id),
) ?? false;
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
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();
handleRefreshButtonPressHook(name, data);
};
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();
}, []);
useFetchDataOnMount(
data,
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
);
const handleOnNewValue = async (
newValue: string | string[] | boolean | Object[],
skipSnapshot: boolean | undefined = false,
): Promise<void> => {
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();
handleOnNewValueHook(newValue, skipSnapshot);
};
const updateNodeInternals = useUpdateNodeInternals();
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
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();
handleNodeClassHook(newNodeClass, code);
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
// @ts-ignore
infoHtml.current = (
@ -265,88 +154,7 @@ export default function ParameterComponent({
if (groupedObj && groupedObj.length > 0) {
//@ts-ignore
refHtml.current = groupedObj.map((item, index) => {
const Icon: any =
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>
);
return <TooltipRenderComponent index={index} item={item} left={left} />;
});
} else {
//@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 && optionalHandle.length === 0) {
optionalHandle = null;
@ -363,6 +172,9 @@ export default function ParameterComponent({
useEffect(() => {
renderTooltips();
}, [tooltipTitle, flow]);
console.log(left === true && type === "dict");
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
@ -428,11 +240,12 @@ export default function ParameterComponent({
(left ? "" : " justify-end")
}
>
{!left && data.node?.frozen && (
<Case condition={left && data.node?.frozen}>
<div className="pr-1">
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div>
)}
</Case>
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
@ -478,45 +291,37 @@ export default function ParameterComponent({
}`}
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
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",
)}
style={{
borderColor: color ?? nodeColors.unknown,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
style={{ borderColor: color ?? nodeColors.unknown }}
onClick={() => setFilterEdge(groupedEdge.current)}
/>
</ShadTooltip>
</div>
</Button>
)}
{left === true &&
type === "str" &&
!data.node?.template[name].options ? (
<Case
condition={
left === true &&
type === "str" &&
!data.node?.template[name]?.options
}
>
<div className="w-full">
{data.node?.template[name].list ? (
<Case condition={data.node?.template[name]?.list}>
<div
className={
// Commenting this out until we have a better
// way to display
// (data.node?.template[name].refresh ? "w-5/6 " : "") +
// (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
"flex-grow"
}
>
@ -524,39 +329,27 @@ export default function ParameterComponent({
componentName={name}
disabled={disabled}
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}
/>
{/* {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>
) : data.node?.template[name].multiline ? (
</Case>
<Case condition={data.node?.template[name]?.multiline}>
<div className="mt-2 flex w-full flex-col ">
<div className="flex-grow">
<TextAreaComponent
disabled={disabled}
value={data.node.template[name].value ?? ""}
value={data.node!.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
id={"textarea-" + data.node!.template[name]?.name}
data-testid={"textarea-" + data.node!.template[name]?.name}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="flex-grow">
<RefreshButton
isLoading={isLoading}
@ -564,7 +357,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons mt-1"
@ -574,12 +367,18 @@ export default function ParameterComponent({
</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={
"flex-grow " +
(data.node?.template[name].refresh_button ? "w-5/6" : "")
(data.node?.template[name]?.refresh_button ? "w-5/6" : "")
}
>
<InputGlobalComponent
@ -599,7 +398,7 @@ export default function ParameterComponent({
data={data}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
@ -607,7 +406,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
@ -617,52 +416,61 @@ export default function ParameterComponent({
</div>
)}
</div>
)}
</Case>
</div>
) : left === true && type === "bool" ? (
</Case>
<Case condition={left === true && type === "bool"}>
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + name}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
enabled={data.node?.template[name]?.value ?? false}
setEnabled={handleOnNewValue}
size="large"
editNode={false}
/>
</div>
) : left === true && type === "float" ? (
</Case>
<Case condition={left === true && type === "float"}>
<div className="mt-2 w-full">
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue}
/>
</div>
) : left === true &&
type === "str" &&
(data.node?.template[name].options ||
data.node?.template[name]?.real_time_refresh) ? (
// TODO: Improve CSS
</Case>
<Case
condition={
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="w-5/6 flex-grow">
<Dropdown
disabled={disabled}
isLoading={isLoading}
options={data.node.template[name].options}
options={data.node!.template[name]?.options}
onSelect={handleOnNewValue}
value={data.node.template[name].value}
value={data.node!.template[name]?.value}
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
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"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
@ -670,46 +478,54 @@ export default function ParameterComponent({
</div>
)}
</div>
) : left === true && type === "code" ? (
</Case>
<Case condition={left === true && type === "code"}>
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
data.node?.flow && data.node.template[name]?.dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
dynamic={data.node?.template[name]?.dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + name}
/>
</div>
) : left === true && type === "file" ? (
</Case>
<Case condition={left === true && type === "file"}>
<div className="mt-2 w-full">
<InputFileComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes}
fileTypes={data.node?.template[name]?.fileTypes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
></InputFileComponent>
</div>
) : left === true && type === "int" ? (
</Case>
<Case condition={left === true && type === "int"}>
<div className="mt-2 w-full">
<IntComponent
rangeSpec={data.node?.template[name].rangeSpec}
rangeSpec={data.node?.template[name]?.rangeSpec}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + name}
/>
</div>
) : left === true && type === "prompt" ? (
</Case>
<Case condition={left === true && type === "prompt"}>
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
@ -717,57 +533,59 @@ export default function ParameterComponent({
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + name}
data-testid={"prompt-input-" + name}
/>
</div>
) : left === true && type === "NestedDict" ? (
</Case>
<Case condition={left === true && type === "NestedDict"}>
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={false}
value={
!data.node!.template[name].value ||
data.node!.template[name].value?.toString() === "{}"
!data.node!.template[name]?.value ||
data.node!.template[name]?.value?.toString() === "{}"
? {
// yourkey: "value",
}
: data.node!.template[name].value
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
id="div-dict-input"
/>
</div>
) : left === true && type === "dict" ? (
</Case>
<Case condition={left === true && type === "dict"}>
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
data.node!.template[name]?.value?.length === 0 ||
!data.node!.template[name]?.value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
: convertObjToArray(data.node!.template[name]?.value, type!)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
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
if (data.node?.template[name].list) {
if (data.node?.template[name]?.list) {
handleOnNewValue(valueToNumbers);
} else handleOnNewValue(valueToNumbers[0]);
}}
isList={data.node?.template[name].list ?? false}
isList={data.node?.template[name]?.list ?? false}
/>
</div>
) : (
<></>
)}
</Case>
</>
</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 [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
data.node?.description!,
);
const [isOutdated, setIsOutdated] = useState(false);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
(state) => state.flowBuildStatus[data.id]?.status,
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp
(state) => state.flowBuildStatus[data.id]?.timestamp,
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
@ -118,7 +118,7 @@ export default function GenericNode({
updateNodeInternals(data.id);
},
[data.id, data.node, setNode, setIsOutdated]
[data.id, data.node, setNode, setIsOutdated],
);
if (!data.node!.template) {
@ -258,7 +258,7 @@ export default function GenericNode({
const isDark = useDarkStore((state) => state.dark);
const renderIconStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
validationStatus: validationStatusType | null,
) => {
if (buildStatus === BuildStatus.BUILDING) {
return <Loading className="text-medium-indigo" />;
@ -299,11 +299,11 @@ export default function GenericNode({
};
const getSpecificClassFromBuildStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
validationStatus: validationStatusType | null,
) => {
let isInvalid = validationStatus && !validationStatus.valid;
if (buildStatus === BuildStatus.INACTIVE && isInvalid) {
if (buildStatus === BuildStatus.INACTIVE) {
// INACTIVE should have its own class
return "inactive-status";
}
@ -323,19 +323,20 @@ export default function GenericNode({
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
validationStatus: validationStatusType | null,
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus
validationStatus,
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
return classNames(
baseBorderClass,
nodeSizeClass,
"generic-node-div",
specificClassFromBuildStatus
specificClassFromBuildStatus,
);
};
@ -383,7 +384,6 @@ export default function GenericNode({
isOutdated,
selected,
]);
return (
<>
{memoizedNodeToolbarComponent}
@ -392,7 +392,7 @@ export default function GenericNode({
selected,
showNode,
buildStatus,
validationStatus
validationStatus,
)}
>
{data.node?.beta && showNode && (
@ -537,7 +537,7 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
@ -565,7 +565,7 @@ export default function GenericNode({
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
)
),
)}
<ParameterComponent
key={scapedJSONStringfy({
@ -722,7 +722,7 @@ export default function GenericNode({
!data.node?.description) &&
nameEditable
? "font-light italic"
: ""
: "",
)}
onDoubleClick={(e) => {
setInputDescription(true);
@ -784,13 +784,13 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n"
"\n",
) ?? data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
@ -817,7 +817,7 @@ export default function GenericNode({
<div
className={classNames(
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
className={classNames(
"form-modal-chat-position",
chat.isSend ? "" : " "
chat.isSend ? "" : " ",
)}
>
<div
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={cn(
"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
@ -210,12 +210,12 @@ dark:prose-invert"
children[0] = (children[0] as string).replace(
"`▍`",
"▍"
"▍",
);
}
const match = /language-(\w+)/.exec(
className || ""
className || "",
);
return !inline ? (
@ -230,7 +230,7 @@ dark:prose-invert"
language: (match && match[1]) || "",
code: String(children).replace(
/\n$/,
""
"",
),
},
]}
@ -248,7 +248,7 @@ dark:prose-invert"
{chatMessage}
</Markdown>
),
[chat.message, chatMessage]
[chat.message, chatMessage],
)}
</div>
{chat.files && (
@ -306,7 +306,7 @@ dark:prose-invert"
parts.push(
<span className="chat-message-highlight">
{chat.message[match[1]]}
</span>
</span>,
);
}
@ -324,7 +324,7 @@ dark:prose-invert"
</>
) : (
<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={
"chat-message-" + chat.sender_name + "-" + chatMessage
}

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { GradientGroup } from "../../../../icons/GradientSparkles";
import { validateSelection } from "../../../../utils/reactflowUtils";
import useFlowStore from "../../../../stores/flowStore";
import { validateSelection } from "../../../../utils/reactflowUtils";
export default function SelectionMenu({
onClick,
nodes,
@ -62,7 +62,11 @@ export default function SelectionMenu({
}
>
<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}
disabled={disable}
>

View file

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

View file

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

View file

@ -32,11 +32,11 @@ import { gradients } from "../../../../utils/styleUtils";
export default function GeneralPage() {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
(state) => state.setCurrentFlowId,
);
const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE
CONTROL_PATCH_USER_STATE,
);
const { autoLogin } = useContext(AuthContext);
@ -113,110 +113,109 @@ export default function GeneralPage() {
</div>
<div className="grid gap-6">
<Form.Root
onSubmit={(event) => {
handlePatchGradient();
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Profile Gradient</CardTitle>
<CardDescription>
Choose the gradient that appears as your profile picture.
</CardDescription>
</CardHeader>
<CardContent>
<div className="py-2">
<GradientChooserComponent
value={
gradient == ""
? userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) %
gradients.length
]
: gradient
}
<Form.Root
onSubmit={(event) => {
handlePatchGradient();
event.preventDefault();
}}
>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Profile Gradient</CardTitle>
<CardDescription>
Choose the gradient that appears as your profile picture.
</CardDescription>
</CardHeader>
<CardContent>
<div className="py-2">
<GradientChooserComponent
value={
gradient == ""
? userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: 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) => {
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
className="field-invalid"
match="valueMissing"
>
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
<Form.Message
className="field-invalid"
match="valueMissing"
>
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</CardContent>
<CardFooter className="border-t px-6 py-4">
<Form.Submit asChild>
<Button type="submit">Save</Button>
</Form.Submit>
</CardFooter>
</Card>
</Form.Root>
)}
</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 { useDarkStore } from "./darkStore";
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
const useFlowStore = create<FlowStoreType>((set, get) => ({
@ -465,8 +464,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
status: BuildStatus,
runId: string,
) {
console.log("handleBuildUpdate", vertexBuildData, status, runId);
if (vertexBuildData && vertexBuildData.inactivated_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices);
get().updateBuildStatus(
vertexBuildData.inactivated_vertices,
BuildStatus.INACTIVE,
);
}
if (vertexBuildData.next_vertices_ids) {
@ -483,9 +487,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const next_vertices_ids = vertexBuildData.next_vertices_ids.filter(
(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(
next_vertices_ids,
vertexBuildData.top_level_vertices,
top_level_vertices,
).map(([id, reference]) => ({ id: id!, reference }));
const newLayers = [
@ -502,10 +510,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
runId: runId,
verticesToRun: get().verticesBuild!.verticesToRun,
});
get().updateBuildStatus(
vertexBuildData.top_level_vertices,
BuildStatus.TO_BUILD,
);
get().updateBuildStatus(top_level_vertices, BuildStatus.TO_BUILD);
}
get().addDataToFlowPool(

View file

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

View file

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

View file

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

View file

@ -21,11 +21,11 @@ export type GlobalVariablesStore = {
id: string,
type?: string,
default_fields?: string[],
value?: string
value?: string,
) => void;
removeGlobalVariable: (name: string) => Promise<void>;
getVariableId: (name: string) => string | undefined;
unavaliableFields: {[name: string]: string};
setUnavaliableFields: (fields: {[name: string]: string}) => void;
unavaliableFields: { [name: string]: string };
setUnavaliableFields: (fields: { [name: string]: 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);
}
export function convertObjToArray(singleObject: object | string) {
export function convertObjToArray(singleObject: object | string, type: string) {
if (type !== "dict") return [{ "": "" }];
if (typeof singleObject === "string") {
singleObject = JSON.parse(singleObject);
}
@ -736,28 +737,30 @@ export function validateSelection(
selection: OnSelectionChangeParams,
edges: Edge[],
): Array<string> {
const clonedSelection = cloneDeep(selection);
const clonedEdges = cloneDeep(edges);
//add edges to selection if selection mode selected only nodes
if (selection.edges.length === 0) {
selection.edges = edges;
if (clonedSelection.edges.length === 0) {
clonedSelection.edges = clonedEdges;
}
// get only edges that are connected to the nodes in the selection
// first creates a set of all the nodes ids
let nodesSet = new Set(selection.nodes.map((n) => n.id));
let nodesSet = new Set(clonedSelection.nodes.map((n) => n.id));
// 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),
);
// add the edges to the selection
selection.edges = connectedEdges;
clonedSelection.edges = connectedEdges;
let errorsArray: Array<string> = [];
// 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");
}
if (
selection.nodes.some(
clonedSelection.nodes.some(
(node) =>
isInputNode(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
if (
selection.nodes.filter(
(n) => !selection.edges.some((e) => e.source === n.id),
clonedSelection.nodes.filter(
(n) => !clonedSelection.edges.some((e) => e.source === n.id),
).length > 1
) {
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
if (
selection.nodes.some(
clonedSelection.nodes.some(
(node) =>
!selection.edges.some((edge) => edge.target === node.id) &&
!selection.edges.some((edge) => edge.source === node.id),
!clonedSelection.edges.some((edge) => edge.target === node.id) &&
!clonedSelection.edges.some((edge) => edge.source === node.id),
)
) {
errorsArray.push("Please select only nodes that are connected");

View file

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

View file

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

View file

@ -190,6 +190,7 @@ export function getPythonApiCode(
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests
from typing import Optional
@ -197,7 +198,7 @@ BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary
# 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,
flow_id: str,
@ -247,6 +248,8 @@ export function getCurlCode(
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `curl -X POST \\
${window.location.protocol}//${
window.location.host
@ -257,7 +260,7 @@ export function getCurlCode(
-d '{"input_value": "message",
"output_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
*/
export function getPythonCode(flowName: string, tweaksBuildedObject): string {
const tweaksObject = tweaksBuildedObject[0];
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",
input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}
@ -481,6 +487,120 @@ export function sensitiveSort(a: string, b: string): number {
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
export function getSetFromObject(obj: object, key?: string): 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.waitForTimeout(2000);
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.waitForTimeout(5000);
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.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.waitForTimeout(1000);
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.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.waitForTimeout(1000);
await page.getByText("Successfully").first().isVisible();

View file

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

View file

@ -16,7 +16,7 @@ test.describe("save component tests", () => {
}
while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click();
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}
@ -27,7 +27,7 @@ test.describe("save component tests", () => {
// Read your file into a buffer.
const jsonContent = readFileSync(
"tests/end-to-end/assets/flow_group_test.json",
"utf-8"
"utf-8",
);
// Create the DataTransfer and File
@ -49,7 +49,7 @@ test.describe("save component tests", () => {
"drop",
{
dataTransfer,
}
},
);
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]')
.click();
await page.getByTestId("title-PythonFunctionTool").click({
modifiers: ["Control"],
});
await page.getByTestId("title-ChatOpenAI").click({
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
.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.waitForTimeout(2000);
@ -165,7 +165,7 @@ test("should like and add components and flows", async ({ page }) => {
await page
.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.waitForTimeout(2000);
@ -174,6 +174,8 @@ test("should like and add components and flows", async ({ page }) => {
await page.waitForTimeout(2000);
await page.getByText("API Key Error").isHidden();
await page.waitForTimeout(5000);
const likedValue = await page
.getByTestId("likes-Website Content QA")
.innerText();
@ -214,7 +216,7 @@ test("should like and add components and flows", async ({ page }) => {
await page.getByText("My Collection").click();
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();
});
@ -233,7 +235,7 @@ test("should share component with share button", async ({ page }) => {
}
while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click();
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
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(
"Attention: API keys in specified fields are automatically removed upon sharing."
"Attention: API keys in specified fields are automatically removed upon sharing.",
)
.isVisible();
await page.getByText("Export").first().isVisible();

View file

@ -13,7 +13,7 @@ test("curl_api_generation", async ({ page, context }) => {
}
while (modalCount === 0) {
await page.locator('//*[@id="new-project-btn"]').click();
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
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) {
await page.locator('//*[@id="new-project-btn"]').click();
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}

View file

@ -10,7 +10,6 @@ import orjson
import pytest
from fastapi.testclient import TestClient
from httpx import AsyncClient
from sqlmodel import Session, SQLModel, create_engine, select
from langflow.graph.graph.base import Graph
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
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.utils import session_getter
from langflow.services.deps import get_db_service
from sqlmodel import Session, SQLModel, create_engine, select
from sqlmodel.pool import StaticPool
from typer.testing import CliRunner
@ -26,7 +26,8 @@ if TYPE_CHECKING:
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"
pytest.BASIC_EXAMPLE_PATH = data_path / "basic_example.json"

View file

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

View file

@ -3,8 +3,7 @@ from typing import Dict, List, Optional
import pytest
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, format_dict, get_base_classes, get_default_factory
from langflow.utils.util import build_template_from_function, get_base_classes, get_default_factory
from pydantic import BaseModel
@ -88,171 +87,6 @@ def test_build_template_from_class():
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
def test_get_base_classes():
base_classes_parent = get_base_classes(Parent)