CRUD of User Management, JWT Management Session, Protected Routes (#758)

This pull request introduces a comprehensive set of features aimed at
enhancing user management, session handling using JWT, and implementing
protected routes within the application. The changes focus on providing
a seamless user experience while ensuring security and controlled access
to resources.
This commit is contained in:
anovazzi1 2023-08-22 16:55:27 -03:00 committed by GitHub
commit db96a73420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
148 changed files with 11298 additions and 3694 deletions

View file

@ -15,7 +15,8 @@
"actboy168.tasks",
"GitHub.copilot",
"ms-python.python",
"eamodio.gitlens"
"eamodio.gitlens",
"GitHub.vscode-pull-request-github"
]
}
},

View file

@ -3,32 +3,36 @@
{
"name": "LangChain Dev Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
"image": "mcr.microsoft.com/devcontainers/python:1-3.10-bullseye",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {}
"ghcr.io/devcontainers/features/node": {},
"ghcr.io/devcontainers-contrib/features/poetry": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "make setup_devcontainer",
"containerEnv": {
"POETRY_VIRTUALENVS_IN_PROJECT": "true"
},
// Configure tool-specific properties.
"customizations": {
"vscode": {"extensions": [
"actboy168.tasks",
"GitHub.copilot",
"ms-python.python",
"sourcery.sourcery",
"eamodio.gitlens"
"eamodio.gitlens",
"ms-vscode.makefile-tools",
"GitHub.vscode-pull-request-github"
]}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "poetry install"
// Configure tool-specific properties.
// "customizations": {},
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"

24
.gitattributes vendored Normal file
View file

@ -0,0 +1,24 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
*.py text
*.js text
*.ts text
*.jsx text
*.md text
*.mdx text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.mp4 binary
*.svg binary
*.ico binary
*.gif binary

View file

@ -34,13 +34,25 @@ lint:
install_frontend:
cd src/frontend && npm install
install_frontendc:
cd src/frontend && rm -rf node_modules package-lock.json && npm install
run_frontend:
cd src/frontend && npm start
setup_devcontainer:
make init
make build_frontend
poetry run langflow --path src/frontend/build
frontend:
make install_frontend
make run_frontend
frontendc:
make install_frontendc
make run_frontend
install_backend:
poetry install

View file

@ -1,33 +1,33 @@
version: "3.4"
services:
backend:
volumes:
- ./:/app
build:
context: ./
dockerfile: ./dev.Dockerfile
command:
[
"sh",
"-c",
"pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload",
]
ports:
- 7860:7860
- 5678:5678
restart: on-failure
frontend:
build:
context: ./src/frontend
dockerfile: ./dev.Dockerfile
args:
- BACKEND_URL=http://backend:7860
ports:
- "3000:3000"
volumes:
- ./src/frontend/public:/home/node/app/public
- ./src/frontend/src:/home/node/app/src
- ./src/frontend/package.json:/home/node/app/package.json
restart: on-failure
version: "3.4"
services:
backend:
volumes:
- ./:/app
build:
context: ./
dockerfile: ./dev.Dockerfile
command:
[
"sh",
"-c",
"pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload",
]
ports:
- 7860:7860
- 5678:5678
restart: on-failure
frontend:
build:
context: ./src/frontend
dockerfile: ./dev.Dockerfile
args:
- BACKEND_URL=http://backend:7860
ports:
- "3000:3000"
volumes:
- ./src/frontend/public:/home/node/app/public
- ./src/frontend/src:/home/node/app/src
- ./src/frontend/package.json:/home/node/app/package.json
restart: on-failure

View file

@ -6,4 +6,58 @@ import Admonition from '@theme/Admonition';
<p>
We appreciate your understanding as we polish our documentation it may contain some rough edges. Share your feedback or report issues to help us improve! 🛠️📝
</p>
</Admonition>
</Admonition>
### BingSearchRun
Bing Search is a web search engine owned and operated by Microsoft. It provides search results for various types of content, including web pages, images, videos, and news articles. It uses a combination of algorithms and human editors to deliver search results to users.
**Params**
- **Api Wrapper:** A BingSearchAPIWrapper component that takes the search URL and a subscription key.
### Calculator
The calculator tool provides mathematical calculation capabilities to an agent by leveraging an LLMMathChain. It allows the agent to perform math when needed to answer questions.
**Params**
- **LLM:** Language Model to use in the calculation.
### GoogleSearchResults
A wrapper around Google Search. Useful for when the user needs to answer questions about with more control over the JSON data returned from the API. It returns the full JSON response configured based on the parameters passed to the API wrapper.
**Params**
- **Api Wrapper:** A GoogleSearchAPIWrapper with Google API key and CSE ID
### GoogleSearchRun
A quick wrapper around Google Search. It executes the search query and returns just the first result snippet from the highest-priority result type.
**Params**
- **Api Wrapper:** A GoogleSearchAPIWrapper with Google API key and CSE ID
### GoogleSerperRun
A low-cost Google Search API.
**Params**
- **Api Wrapper:** A GoogleSerperAPIWrapper component with API key and result keys
### InfoSQLDatabaseTool
Tool for getting metadata about a SQL database. The input to this tool is a comma-separated list of tables, and the output is the schema and sample rows for those tables. Example Input: `“table1`, `table2`, `table3”`.
**Params**
- **Db:** SQLDatabase to query.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Before After
Before After

1551
package-lock.json generated

File diff suppressed because it is too large Load diff

152
poetry.lock generated
View file

@ -146,13 +146,13 @@ files = [
[[package]]
name = "alembic"
version = "1.11.2"
version = "1.11.3"
description = "A database migration tool for SQLAlchemy."
optional = false
python-versions = ">=3.7"
files = [
{file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"},
{file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"},
{file = "alembic-1.11.3-py3-none-any.whl", hash = "sha256:d6c96c2482740592777c400550a523bc7a9aada4e210cae2e733354ddae6f6f8"},
{file = "alembic-1.11.3.tar.gz", hash = "sha256:3db4ce81a9072e1b5aa44c2d202add24553182672a12daf21608d6f62a8f9cf9"},
]
[package.dependencies]
@ -165,20 +165,20 @@ tz = ["python-dateutil"]
[[package]]
name = "anthropic"
version = "0.3.9"
version = "0.3.10"
description = "Client library for the anthropic API"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "anthropic-0.3.9-py3-none-any.whl", hash = "sha256:23e8daf266c707faa0b85328ada03dcf885e62c2f236eb0159352e29b9c4e2e3"},
{file = "anthropic-0.3.9.tar.gz", hash = "sha256:c19b75308c07cb1ecbea03ffcdde9ba68e5ca22a22479217712396385678bde3"},
{file = "anthropic-0.3.10-py3-none-any.whl", hash = "sha256:95cd73168296a4f91a5899a28660991e044322cf94442d07b99901d4ca74acd6"},
{file = "anthropic-0.3.10.tar.gz", hash = "sha256:d1f66efc541fbff0ecfd37fd4d3690f9daaa748fc42d9ded5863a10815a5d97b"},
]
[package.dependencies]
anyio = ">=3.5.0,<4"
distro = ">=1.7.0,<2"
httpx = ">=0.23.0,<1"
pydantic = ">=1.9.0,<2.0.0"
pydantic = ">=1.9.0,<3"
tokenizers = ">=0.13.0"
typing-extensions = ">=4.5,<5"
@ -792,13 +792,13 @@ sqlalchemy = ["sqlalchemy (>1.3.21,<2.0)"]
[[package]]
name = "cohere"
version = "4.20.0"
version = "4.20.1"
description = ""
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "cohere-4.20.0-py3-none-any.whl", hash = "sha256:bebe4b1d21da0719aaa7db2cc180b5f3d0a802c19ad2d893d627971286e50497"},
{file = "cohere-4.20.0.tar.gz", hash = "sha256:a16e86981945c201bab67ad6d47e57f42a37bf2a774f6ac6b8b12233aae6409a"},
{file = "cohere-4.20.1-py3-none-any.whl", hash = "sha256:4466c7abdbb168fe2893e5b5123882fe91691d0e3fe43ee254b411917f780dfc"},
{file = "cohere-4.20.1.tar.gz", hash = "sha256:533e4a45b38dc338f8a27f24e098b652b4f5ed9f9fbb4719780d1a9fcd1af39e"},
]
[package.dependencies]
@ -1630,13 +1630,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.96.0"
version = "2.97.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "google-api-python-client-2.96.0.tar.gz", hash = "sha256:f712373d03d338af57b9f5fe98c91f4b5baaa8765469b015bc623c4681c5bd51"},
{file = "google_api_python_client-2.96.0-py2.py3-none-any.whl", hash = "sha256:38c2b61b10d15bb41ec8f89303e3837ec2d2c3e4e38de5800c05ee322492f937"},
{file = "google-api-python-client-2.97.0.tar.gz", hash = "sha256:48277291894876a1ca7ed4127e055e81f81e6343ced1b544a7200ae2c119dcd7"},
{file = "google_api_python_client-2.97.0-py2.py3-none-any.whl", hash = "sha256:5215f4cd577753fc4192ccfbe0bb8b55d4bb5fd68fa6268ac5cf271b6305de31"},
]
[package.dependencies]
@ -2979,13 +2979,13 @@ test = ["psutil", "pytest", "pytest-asyncio"]
[[package]]
name = "langsmith"
version = "0.0.22"
version = "0.0.24"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = ">=3.8.1,<4.0"
files = [
{file = "langsmith-0.0.22-py3-none-any.whl", hash = "sha256:1bc94a2e5bfa355ca15d9e658c2c2d04c8cc45c61892a1be08a7c3b40f2fd3f4"},
{file = "langsmith-0.0.22.tar.gz", hash = "sha256:5726c7841294db2a9e5863e20718878d16e28722bdaf3169a278ff3bda2f0be7"},
{file = "langsmith-0.0.24-py3-none-any.whl", hash = "sha256:f9f951d070aa1919123d700642aca9c781edfc8797a65ab1161aa12f89bed707"},
{file = "langsmith-0.0.24.tar.gz", hash = "sha256:9c066dd915752324490a735692997b0db0958f5dfc1e0a0dfbf752c6e62c7529"},
]
[package.dependencies]
@ -3573,33 +3573,38 @@ dill = ">=0.3.7"
[[package]]
name = "mypy"
version = "1.5.0"
version = "1.5.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ad3109bec37cc33654de8db30fe8ff3a1bb57ea65144167d68185e6dced9868d"},
{file = "mypy-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ea3a0241cb005b0ccdbd318fb99619b21ae51bcf1660b95fc22e0e7d3ba4a1"},
{file = "mypy-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fe816e26e676c1311b9e04fd576543b873576d39439f7c24c8e5c7728391ecf"},
{file = "mypy-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:42170e68adb1603ccdc55a30068f72bcfcde2ce650188e4c1b2a93018b826735"},
{file = "mypy-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d145b81a8214687cfc1f85c03663a5bbe736777410e5580e54d526e7e904f564"},
{file = "mypy-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c36011320e452eb30bec38b9fd3ba20569dc9545d7d4540d967f3ea1fab9c374"},
{file = "mypy-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3940cf5845b2512b3ab95463198b0cdf87975dfd17fdcc6ce9709a9abe09e69"},
{file = "mypy-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9166186c498170e1ff478a7f540846b2169243feb95bc228d39a67a1a450cdc6"},
{file = "mypy-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:725b57a19b7408ef66a0fd9db59b5d3e528922250fb56e50bded27fea9ff28f0"},
{file = "mypy-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:eec5c927aa4b3e8b4781840f1550079969926d0a22ce38075f6cfcf4b13e3eb4"},
{file = "mypy-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79c520aa24f21852206b5ff2cf746dc13020113aa73fa55af504635a96e62718"},
{file = "mypy-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:769ddb6bfe55c2bd9c7d6d7020885a5ea14289619db7ee650e06b1ef0852c6f4"},
{file = "mypy-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf18f8db7e5f060d61c91e334d3b96d6bb624ddc9ee8a1cde407b737acbca2c"},
{file = "mypy-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a2500ad063413bc873ae102cf655bf49889e0763b260a3a7cf544a0cbbf7e70a"},
{file = "mypy-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:84cf9f7d8a8a22bb6a36444480f4cbf089c917a4179fbf7eea003ea931944a7f"},
{file = "mypy-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a551ed0fc02455fe2c1fb0145160df8336b90ab80224739627b15ebe2b45e9dc"},
{file = "mypy-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:372fd97293ed0076d52695849f59acbbb8461c4ab447858cdaeaf734a396d823"},
{file = "mypy-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8a7444d6fcac7e2585b10abb91ad900a576da7af8f5cffffbff6065d9115813"},
{file = "mypy-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:35b13335c6c46a386577a51f3d38b2b5d14aa619e9633bb756bd77205e4bd09f"},
{file = "mypy-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:2c9d570f53908cbea326ad8f96028a673b814d9dca7515bf71d95fa662c3eb6f"},
{file = "mypy-1.5.0-py3-none-any.whl", hash = "sha256:69b32d0dedd211b80f1b7435644e1ef83033a2af2ac65adcdc87c38db68a86be"},
{file = "mypy-1.5.0.tar.gz", hash = "sha256:f3460f34b3839b9bc84ee3ed65076eb827cd99ed13ed08d723f9083cada4a212"},
{file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"},
{file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"},
{file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"},
{file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"},
{file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"},
{file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"},
{file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"},
{file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"},
{file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"},
{file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"},
{file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"},
{file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"},
{file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"},
{file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"},
{file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"},
{file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"},
{file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"},
{file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"},
{file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"},
{file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"},
{file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"},
{file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"},
{file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"},
{file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"},
{file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"},
{file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"},
{file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"},
]
[package.dependencies]
@ -5890,29 +5895,42 @@ python-versions = "*"
files = [
{file = "safetensors-0.3.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b6a66989075c2891d743153e8ba9ca84ee7232c8539704488f454199b8b8f84d"},
{file = "safetensors-0.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:670d6bc3a3b377278ce2971fa7c36ebc0a35041c4ea23b9df750a39380800195"},
{file = "safetensors-0.3.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:564f42838721925b5313ae864ba6caa6f4c80a9fbe63cf24310c3be98ab013cd"},
{file = "safetensors-0.3.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:7f80af7e4ab3188daaff12d43d078da3017a90d732d38d7af4eb08b6ca2198a5"},
{file = "safetensors-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec30d78f20f1235b252d59cbb9755beb35a1fde8c24c89b3c98e6a1804cfd432"},
{file = "safetensors-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16063d94d8f600768d3c331b1e97964b1bf3772e19710105fe24ec5a6af63770"},
{file = "safetensors-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb44e140bf2aeda98d9dde669dbec15f7b77f96a9274469b91a6cf4bcc5ec3b"},
{file = "safetensors-0.3.2-cp310-cp310-win32.whl", hash = "sha256:2961c1243fd0da46aa6a1c835305cc4595486f8ac64632a604d0eb5f2de76175"},
{file = "safetensors-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c813920482c337d1424d306e1b05824a38e3ef94303748a0a287dea7a8c4f805"},
{file = "safetensors-0.3.2-cp311-cp311-macosx_10_11_universal2.whl", hash = "sha256:707df34bd9b9047e97332136ad98e57028faeccdb9cfe1c3b52aba5964cc24bf"},
{file = "safetensors-0.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:becc5bb85b2947eae20ed23b407ebfd5277d9a560f90381fe2c42e6c043677ba"},
{file = "safetensors-0.3.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:30a75707be5cc9686490bde14b9a371cede4af53244ea72b340cfbabfffdf58a"},
{file = "safetensors-0.3.2-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:54ad6af663e15e2b99e2ea3280981b7514485df72ba6d014dc22dae7ba6a5e6c"},
{file = "safetensors-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37764b3197656ef507a266c453e909a3477dabc795962b38e3ad28226f53153b"},
{file = "safetensors-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4939067736783acd8391d83cd97d6c202f94181951ce697d519f9746381b6a39"},
{file = "safetensors-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada0fac127ff8fb04834da5c6d85a8077e6a1c9180a11251d96f8068db922a17"},
{file = "safetensors-0.3.2-cp311-cp311-win32.whl", hash = "sha256:155b82dbe2b0ebff18cde3f76b42b6d9470296e92561ef1a282004d449fa2b4c"},
{file = "safetensors-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:a86428d196959619ce90197731be9391b5098b35100a7228ef4643957648f7f5"},
{file = "safetensors-0.3.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:c1f8ab41ed735c5b581f451fd15d9602ff51aa88044bfa933c5fa4b1d0c644d1"},
{file = "safetensors-0.3.2-cp37-cp37m-macosx_13_0_x86_64.whl", hash = "sha256:bc9cfb3c9ea2aec89685b4d656f9f2296f0f0d67ecf2bebf950870e3be89b3db"},
{file = "safetensors-0.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace5d471e3d78e0d93f952707d808b5ab5eac77ddb034ceb702e602e9acf2be9"},
{file = "safetensors-0.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de3e20a388b444381bcda1a3193cce51825ddca277e4cf3ed1fe8d9b2d5722cd"},
{file = "safetensors-0.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d7d70d48585fe8df00725aa788f2e64fd24a4c9ae07cd6be34f6859d0f89a9c"},
{file = "safetensors-0.3.2-cp37-cp37m-win32.whl", hash = "sha256:6ff59bc90cdc857f68b1023be9085fda6202bbe7f2fd67d06af8f976d6adcc10"},
{file = "safetensors-0.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8b05c93da15fa911763a89281906ca333ed800ab0ef1c7ce53317aa1a2322f19"},
{file = "safetensors-0.3.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:8969cfd9e8d904e8d3c67c989e1bd9a95e3cc8980d4f95e4dcd43c299bb94253"},
{file = "safetensors-0.3.2-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:f54148ac027556eb02187e9bc1556c4d916c99ca3cb34ca36a7d304d675035c1"},
{file = "safetensors-0.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caec25fedbcf73f66c9261984f07885680f71417fc173f52279276c7f8a5edd3"},
{file = "safetensors-0.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50224a1d99927ccf3b75e27c3d412f7043280431ab100b4f08aad470c37cf99a"},
{file = "safetensors-0.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa98f49e95f02eb750d32c4947e7d5aa43883149ebd0414920866446525b70f0"},
{file = "safetensors-0.3.2-cp38-cp38-win32.whl", hash = "sha256:33409df5e28a83dc5cc5547a3ac17c0f1b13a1847b1eb3bc4b3be0df9915171e"},
{file = "safetensors-0.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:e04a7cbbb3856159ab99e3adb14521544f65fcb8548cce773a1435a0f8d78d27"},
{file = "safetensors-0.3.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:7c864cf5dcbfb608c5378f83319c60cc9c97263343b57c02756b7613cd5ab4dd"},
{file = "safetensors-0.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:14e8c19d6dc51d4f70ee33c46aff04c8ba3f95812e74daf8036c24bc86e75cae"},
{file = "safetensors-0.3.2-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:042a60f633c3c7009fdf6a7c182b165cb7283649d2a1e9c7a4a1c23454bd9a5b"},
{file = "safetensors-0.3.2-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:fafd95e5ef41e8f312e2a32b7031f7b9b2a621b255f867b221f94bb2e9f51ae8"},
{file = "safetensors-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ed77cf358abce2307f03634694e0b2a29822e322a1623e0b1aa4b41e871bf8b"},
{file = "safetensors-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d344e8b2681a33aafc197c90b0def3229b3317d749531c72fa6259d0caa5c8c"},
{file = "safetensors-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87ff0024ef2e5722a79af24688ce4a430f70601d0cf712a744105ed4b8f67ba5"},
{file = "safetensors-0.3.2-cp39-cp39-win32.whl", hash = "sha256:827af9478b78977248ba93e2fd97ea307fb63f463f80cef4824460f8c2542a52"},
{file = "safetensors-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9b09f27c456efa301f98681ea14b12f81f2637889f6336223ccab71e42c34541"},
@ -6113,18 +6131,18 @@ files = [
[[package]]
name = "setuptools"
version = "68.0.0"
version = "68.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
{file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
{file = "setuptools-68.1.0-py3-none-any.whl", hash = "sha256:e13e1b0bc760e9b0127eda042845999b2f913e12437046e663b833aa96d89715"},
{file = "setuptools-68.1.0.tar.gz", hash = "sha256:d59c97e7b774979a5ccb96388efc9eb65518004537e85d52e81eaee89ab6dd91"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
@ -6514,13 +6532,13 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"]
[[package]]
name = "textual"
version = "0.32.0"
version = "0.33.0"
description = "Modern Text User Interface framework"
optional = true
python-versions = ">=3.7,<4.0"
files = [
{file = "textual-0.32.0-py3-none-any.whl", hash = "sha256:81fc68406c8806bc864e2f035874a868b4ff0cf466289dce5f7b31869949383b"},
{file = "textual-0.32.0.tar.gz", hash = "sha256:f7b6683bc18faee6fd3c47cfbad43fbf8273c5fecc12230d52ce5ee089021327"},
{file = "textual-0.33.0-py3-none-any.whl", hash = "sha256:698a093add0fd21c786232bcacde9ff427a9d5dc9ea5deca93437d9453e4ead1"},
{file = "textual-0.33.0.tar.gz", hash = "sha256:e0a98b1d9c4458c5bb4269c65d0a7f3e926f197411242d2f8faf80183d47a728"},
]
[package.dependencies]
@ -6939,6 +6957,17 @@ files = [
{file = "types_cachetools-5.3.0.6-py3-none-any.whl", hash = "sha256:f7f8a25bfe306f2e6bc2ad0a2f949d9e72f2d91036d509c36d3810bf728bc6e1"},
]
[[package]]
name = "types-passlib"
version = "1.7.7.13"
description = "Typing stubs for passlib"
optional = false
python-versions = "*"
files = [
{file = "types-passlib-1.7.7.13.tar.gz", hash = "sha256:f152639f1f2103d7f59a56e2aec5f9398a75a80830991d0d68aac5c2b9c32a77"},
{file = "types_passlib-1.7.7.13-py3-none-any.whl", hash = "sha256:414b5ee9c88313357c9261cfcf816509b1e8e4673f0796bd61e9ef249f6fe076"},
]
[[package]]
name = "types-pillow"
version = "9.5.0.6"
@ -6950,6 +6979,31 @@ files = [
{file = "types_Pillow-9.5.0.6-py3-none-any.whl", hash = "sha256:1d238abaa9d529b04941d805b7f4d3f7df30702bb14521ec507617f117406fb4"},
]
[[package]]
name = "types-pyasn1"
version = "0.4.0.6"
description = "Typing stubs for pyasn1"
optional = false
python-versions = "*"
files = [
{file = "types-pyasn1-0.4.0.6.tar.gz", hash = "sha256:8f1965d0b79152f9d1efc89f9aa9a8cdda7cd28b2619df6737c095cbedeff98b"},
{file = "types_pyasn1-0.4.0.6-py3-none-any.whl", hash = "sha256:dd5fc818864e63a66cd714be0a7a59a493f4a81b87ee9ac978c41f1eaa9a0cef"},
]
[[package]]
name = "types-python-jose"
version = "3.3.4.8"
description = "Typing stubs for python-jose"
optional = false
python-versions = "*"
files = [
{file = "types-python-jose-3.3.4.8.tar.gz", hash = "sha256:3c316675c3cee059ccb9aff87358254344915239fa7f19cee2787155a7db14ac"},
{file = "types_python_jose-3.3.4.8-py3-none-any.whl", hash = "sha256:95592273443b45dc5cc88f7c56aa5a97725428753fb738b794e63ccb4904954e"},
]
[package.dependencies]
types-pyasn1 = "*"
[[package]]
name = "types-pytz"
version = "2023.3.0.1"
@ -7693,4 +7747,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.11"
content-hash = "0f7d3f932eb130e7f3b6d234fc36e403ef409d63cfc825ba9f4c470203425ad2"
content-hash = "8ad605e7ea30f2819dbc03eac6c2e67576a98d1efa4890912414a7568fc27441"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "langflow"
version = "0.4.7"
version = "0.4.11"
description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"]
maintainers = [
@ -97,6 +97,8 @@ pandas-stubs = "^2.0.0.230412"
types-pillow = "^9.5.0.2"
types-appdirs = "^1.4.3.5"
types-pyyaml = "^6.0.12.8"
types-python-jose = "^3.3.4.8"
types-passlib = "^1.7.7.13"
[tool.poetry.extras]

View file

@ -1,6 +1,7 @@
import sys
import time
import httpx
from langflow.services.manager import initialize_settings_manager
from langflow.services.utils import get_settings_manager
from langflow.utils.util import get_number_of_workers
from multiprocess import Process # type: ignore
@ -30,6 +31,7 @@ def update_settings(
"""Update the settings from a config file."""
# Check for database_url in the environment variables
initialize_settings_manager()
settings_manager = get_settings_manager()
if config:
logger.debug(f"Loading settings from {config}")

View file

@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated, Optional
from typing import Annotated, Optional, Union
from langflow.services.cache.utils import save_uploaded_file
from langflow.services.database.models.flow import Flow
@ -44,15 +44,21 @@ def get_all():
logger.info(
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
)
custom_component_dicts = [
build_langchain_custom_component_list_from_path(str(path))
for path in settings_manager.settings.COMPONENTS_PATH
]
custom_component_dicts = []
processed_paths = []
for path in settings_manager.settings.COMPONENTS_PATH:
if str(path) in processed_paths:
continue
custom_component_dict = build_langchain_custom_component_list_from_path(
str(path)
)
custom_component_dicts.append(custom_component_dict)
processed_paths.append(str(path))
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
for custom_component_dict in custom_component_dicts:
# custom_component_dict is a dict of dicts
if not custom_component_dict:
continue
category = list(custom_component_dict.keys())[0]
logger.info(
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
@ -75,6 +81,7 @@ async def process_flow(
inputs: Optional[dict] = None,
tweaks: Optional[dict] = None,
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
session: Session = Depends(get_session),
):
"""
@ -94,10 +101,10 @@ async def process_flow(
graph_data = process_tweaks(graph_data, tweaks)
except Exception as exc:
logger.error(f"Error processing tweaks: {exc}")
response = process_graph_cached(graph_data, inputs, clear_cache)
return ProcessResponse(
result=response,
response, session_id = process_graph_cached(
graph_data, inputs, clear_cache, session_id
)
return ProcessResponse(result=response, session_id=session_id)
except Exception as e:
# Log stack trace
logger.exception(e)

View file

@ -47,6 +47,7 @@ class ProcessResponse(BaseModel):
"""Process response schema."""
result: dict
session_id: Optional[str] = None
class ChatMessage(BaseModel):

View file

@ -7,9 +7,8 @@ from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status
from datetime import datetime, timedelta, timezone
from langflow.services.utils import get_settings_manager
from langflow.services.utils import get_settings_manager, get_session
from langflow.services.utils import get_session
from langflow.database.models.user import (
User,
get_user_by_id,
@ -83,18 +82,40 @@ def create_token(data: dict, expires_delta: timedelta):
)
def create_user_longterm_token(
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
) -> dict:
def create_super_user(db: Session = Depends(get_session)) -> User:
settings_manager = get_settings_manager()
super_user = get_user_by_username(db, settings_manager.settings.FIRST_SUPERUSER)
if not super_user:
super_user = User(
username=settings_manager.settings.FIRST_SUPERUSER,
password=get_password_hash(
settings_manager.settings.FIRST_SUPERUSER_PASSWORD
),
is_superuser=True,
is_active=True,
last_login_at=None,
)
db.add(super_user)
db.commit()
db.refresh(super_user)
return super_user
def create_user_longterm_token(db: Session = Depends(get_session)) -> dict:
super_user = create_super_user(db)
access_token_expires_longterm = timedelta(days=365)
access_token = create_token(
data={"sub": str(user_id)},
data={"sub": str(super_user.id)},
expires_delta=access_token_expires_longterm,
)
# Update: last_login_at
if update_last_login:
update_user_last_login_at(user_id, db)
update_user_last_login_at(super_user.id, db)
return {
"access_token": access_token,
@ -103,6 +124,23 @@ def create_user_longterm_token(
}
def create_user_api_key(user_id: UUID) -> dict:
access_token = create_token(
data={"sub": str(user_id), "role": "api_key"},
expires_delta=timedelta(days=365 * 2),
)
return {"api_key": access_token}
def get_user_id_from_token(token: str) -> UUID:
try:
user_id = jwt.get_unverified_claims(token)["sub"]
return UUID(user_id)
except (KeyError, JWTError, ValueError):
return UUID(int=0)
def create_user_tokens(
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
) -> dict:

View file

@ -0,0 +1,82 @@
from langflow import CustomComponent
from typing import Optional
from langchain.prompts import SystemMessagePromptTemplate
from langchain.tools import Tool
from langchain.schema.memory import BaseMemory
from langchain.chat_models import ChatOpenAI
from langchain.agents.agent import AgentExecutor
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
from langchain.memory.token_buffer import ConversationTokenBufferMemory
from langchain.prompts.chat import MessagesPlaceholder
from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import (
_get_default_system_message,
)
class ConversationalAgent(CustomComponent):
display_name: str = "OpenAI Conversational Agent"
description: str = "Conversational Agent that can use OpenAI's function calling API"
def build_config(self):
openai_function_models = [
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0613",
"gpt-4-32k-0613",
]
return {
"tools": {"is_list": True, "display_name": "Tools"},
"memory": {"display_name": "Memory"},
"system_message": {"display_name": "System Message"},
"max_token_limit": {"display_name": "Max Token Limit"},
"model_name": {
"display_name": "Model Name",
"options": openai_function_models,
"value": openai_function_models[0],
},
"code": {"show": False},
}
def build(
self,
model_name: str,
openai_api_key: str,
openai_api_base: str,
tools: Tool,
memory: Optional[BaseMemory] = None,
system_message: Optional[SystemMessagePromptTemplate] = None,
max_token_limit: int = 2000,
) -> AgentExecutor:
llm = ChatOpenAI(
model=model_name,
openai_api_key=openai_api_key,
openai_api_base=openai_api_base,
)
if not memory:
memory_key = "chat_history"
memory = ConversationTokenBufferMemory(
memory_key=memory_key,
return_messages=True,
output_key="output",
llm=llm,
max_token_limit=max_token_limit,
)
else:
memory_key = memory.memory_key # type: ignore
_system_message = system_message or _get_default_system_message()
prompt = OpenAIFunctionsAgent.create_prompt(
system_message=_system_message, # type: ignore
extra_prompt_messages=[MessagesPlaceholder(variable_name=memory_key)],
)
agent = OpenAIFunctionsAgent(
llm=llm, tools=tools, prompt=prompt # type: ignore
)
return AgentExecutor(
agent=agent,
tools=tools, # type: ignore
memory=memory,
verbose=True,
return_intermediate_steps=True,
)

View file

@ -3,6 +3,10 @@ from typing import Any, Union
from langflow.interface.utils import extract_input_variables_from_prompt
class UnbuiltObject:
pass
def validate_prompt(prompt: str):
"""Validate prompt."""
if extract_input_variables_from_prompt(prompt):

View file

@ -1,4 +1,5 @@
import ast
from langflow.graph.utils import UnbuiltObject
from langflow.interface.initialize import loading
from langflow.interface.listing import lazy_load_dict
from langflow.utils.constants import DIRECT_TYPES
@ -22,7 +23,7 @@ class Vertex:
self.edges: List["Edge"] = []
self.base_type: Optional[str] = base_type
self._parse_data()
self._built_object = None
self._built_object = UnbuiltObject()
self._built = False
self.artifacts: Dict[str, Any] = {}
@ -245,8 +246,14 @@ class Vertex:
"""
Checks if the built object is None and raises a ValueError if so.
"""
if self._built_object is None:
raise ValueError(f"Node type {self.vertex_type} not found")
if isinstance(self._built_object, UnbuiltObject):
raise ValueError(f"{self.vertex_type}: {self._built_object_repr()}")
elif self._built_object is None:
message = f"{self.vertex_type} returned None."
if self.base_type == "custom_components":
message += " Make sure your build method returns a component."
raise ValueError(message)
def build(self, force: bool = False) -> Any:
if not self._built or force:

View file

@ -226,7 +226,12 @@ class PromptVertex(Vertex):
# so the prompt format doesn't break
artifacts.pop("handle_keys", None)
try:
template = self._built_object.template
if not hasattr(self._built_object, "template") and hasattr(
self._built_object, "prompt"
):
template = self._built_object.prompt.template
else:
template = self._built_object.template
for key, value in artifacts.items():
if value:
replace_key = "{" + key + "}"

View file

@ -8,10 +8,13 @@ from langchain.text_splitter import TextSplitter
from langchain.tools import Tool
from langchain.vectorstores.base import VectorStore
from langchain.schema import BaseOutputParser
from langchain.schema.memory import BaseMemory
from langchain.memory.chat_memory import BaseChatMemory
from langchain.agents.agent import AgentExecutor
LANGCHAIN_BASE_TYPES = {
"Chain": Chain,
"AgentExecutor": AgentExecutor,
"Tool": Tool,
"BaseLLM": BaseLLM,
"PromptTemplate": PromptTemplate,
@ -22,6 +25,8 @@ LANGCHAIN_BASE_TYPES = {
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
"BaseOutputParser": BaseOutputParser,
"BaseMemory": BaseMemory,
"BaseChatMemory": BaseChatMemory,
}
# Langchain base types plus Python base types

View file

@ -51,8 +51,8 @@ class CustomComponent(Component, extra=Extra.allow):
for type_hint in TYPE_HINT_LIST:
if reader._is_type_hint_used_in_args(
"Optional", code
) and not reader._is_type_hint_imported("Optional", code):
type_hint, code
) and not reader._is_type_hint_imported(type_hint, code):
error_detail = {
"error": "Type hint Error",
"traceback": f"Type hint '{type_hint}' is used but not imported in the code.",

View file

@ -51,7 +51,9 @@ def handle_partial_variables(prompt, format_kwargs: Dict):
}
# Remove handle_keys otherwise LangChain raises an error
partial_variables.pop("handle_keys", None)
return prompt.partial(**partial_variables)
if partial_variables and hasattr(prompt, "partial"):
return prompt.partial(**partial_variables)
return prompt
def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict):

View file

@ -1,3 +1,4 @@
from typing import Any, Dict, Tuple
from langflow.services.cache.utils import memoize_dict
from langflow.graph import Graph
from langflow.utils.logger import logger
@ -15,7 +16,7 @@ def build_langchain_object_with_caching(data_graph):
@memoize_dict(maxsize=10)
def build_sorted_vertices_with_caching(data_graph):
def build_sorted_vertices_with_caching(data_graph) -> Tuple[Any, Dict]:
"""
Build langchain object from data_graph.
"""

View file

@ -6,7 +6,7 @@ from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from langflow.api import router
from langflow.routers import login, users, health
from langflow.routers import api_key, login, users, health
from langflow.interface.utils import setup_llm_caching
from langflow.services.database.utils import initialize_database
@ -32,6 +32,7 @@ def create_app():
)
app.include_router(login.router)
app.include_router(api_key.router)
app.include_router(users.router)
app.include_router(health.router)

View file

@ -5,6 +5,7 @@ from langflow.api.v1.callback import (
)
from langflow.processing.process import fix_memory_inputs, format_actions
from langflow.utils.logger import logger
from langchain.agents.agent import AgentExecutor
async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwargs):
@ -20,7 +21,8 @@ async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwa
# to display intermediate steps
langchain_object.return_intermediate_steps = True
try:
fix_memory_inputs(langchain_object)
if not isinstance(langchain_object, AgentExecutor):
fix_memory_inputs(langchain_object)
except Exception as exc:
logger.error(f"Error fixing memory inputs: {exc}")

View file

@ -85,39 +85,55 @@ def get_input_str_if_only_one_input(inputs: dict) -> Optional[str]:
return list(inputs.values())[0] if len(inputs) == 1 else None
def process_graph_cached(
data_graph: Dict[str, Any], inputs: Optional[dict] = None, clear_cache=False
):
"""
Process graph by extracting input variables and replacing ZeroShotPrompt
with PromptTemplate,then run the graph and return the result and thought.
"""
# Load langchain object
def get_build_result(data_graph, session_id):
# If session_id is provided, load the langchain_object from the session
# using build_sorted_vertices_with_caching.get_result_by_session_id
# if it returns something different than None, return it
# otherwise, build the graph and return the result
if session_id:
logger.debug(f"Loading LangChain object from session {session_id}")
result = build_sorted_vertices_with_caching.get_result_by_session_id(session_id)
if result is not None:
logger.debug("Loaded LangChain object")
return result
logger.debug("Building langchain object")
return build_sorted_vertices_with_caching(data_graph)
def clear_caches_if_needed(clear_cache: bool):
if clear_cache:
build_sorted_vertices_with_caching.clear_cache()
logger.debug("Cleared cache")
langchain_object, artifacts = build_sorted_vertices_with_caching(data_graph)
logger.debug("Loaded LangChain object")
if inputs is None:
inputs = {}
# Add artifacts to inputs
# artifacts can be documents loaded when building
# the flow
for (
key,
value,
) in artifacts.items():
if key not in inputs or not inputs[key]:
inputs[key] = value
def load_langchain_object(
data_graph: Dict[str, Any], session_id: str
) -> Tuple[Union[Chain, VectorStore], Dict[str, Any], str]:
langchain_object, artifacts = get_build_result(data_graph, session_id)
session_id = build_sorted_vertices_with_caching.hash
logger.debug("Loaded LangChain object")
if langchain_object is None:
# Raise user facing error
raise ValueError(
"There was an error loading the langchain_object. Please, check all the nodes and try again."
)
# Generate result and thought
return langchain_object, artifacts, session_id
def process_inputs(inputs: Optional[dict], artifacts: Dict[str, Any]) -> dict:
if inputs is None:
inputs = {}
for key, value in artifacts.items():
if key not in inputs or not inputs[key]:
inputs[key] = value
return inputs
def generate_result(langchain_object: Union[Chain, VectorStore], inputs: dict):
if isinstance(langchain_object, Chain):
if inputs is None:
raise ValueError("Inputs must be provided for a Chain")
@ -130,9 +146,28 @@ def process_graph_cached(
raise ValueError(
f"Unknown langchain_object type: {type(langchain_object).__name__}"
)
return result
def process_graph_cached(
data_graph: Dict[str, Any],
inputs: Optional[dict] = None,
clear_cache=False,
session_id=None,
) -> Tuple[Any, str]:
clear_caches_if_needed(clear_cache)
# If session_id is provided, load the langchain_object from the session
# else build the graph and return the result and the new session_id
langchain_object, artifacts, session_id = load_langchain_object(
data_graph, session_id
)
processed_inputs = process_inputs(inputs, artifacts)
result = generate_result(langchain_object, processed_inputs)
return result, session_id
def load_flow_from_json(
flow: Union[Path, str, dict], tweaks: Optional[dict] = None, build=True
):

View file

@ -0,0 +1,49 @@
from fastapi import APIRouter
router = APIRouter(tags=["APIKey"])
@router.get("/api_key/{user_id}")
def get_api_key(user_id: str):
return {
"total_count": 3,
"user_id": user_id,
"api_keys": [
{
"id": "4425707e-cce4-4d1b-a54e-bd2632064657",
"api_key": "lf-...abcd",
"name": "my api_key name - 01",
"created_at": "2023-08-15T19:28:40.019613",
"last_used_at": "2023-08-16T18:38:20.875210",
},
{
"id": "6fb7282b-9f2e-4efe-9bda-0c3d8f899473",
"api_key": "lf-...abcd",
"name": "my api_key name - 02",
"created_at": "2023-08-15T19:41:30.077942",
"last_used_at": "2023-08-15T19:45:32.067899",
},
{
"id": "c55f3b32-4920-42b6-a5cd-698b4251806e",
"api_key": "lf-...abcd",
"name": "my api_key name - 03",
"created_at": "2023-08-15T20:29:40.577808",
"last_used_at": "2023-08-15T20:29:40.577816",
},
],
}
@router.post("/api_key/{user_id}")
def create_api_key(user_id: str):
return {
"user_id": user_id,
"name": "my api-key 01",
"api_key": "lf-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YTBmODM1ZS0yMTQxLTQ2YWItYmQ4NS0yMWEzMjQ1MTE2ZDAiLCJleHAiOjE2OTIyMTUwMTN9.c_s0ZPRtjSI9yUrhi8ACIwyXf0feRLYfaeIZEbRVKQg",
}
@router.delete("/api_key/{api_key_id}")
def delete_api_key(api_key_id: str):
return {"detail": "API Key deleted"}

View file

@ -1,4 +1,3 @@
from uuid import UUID
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
@ -38,8 +37,7 @@ async def auto_login(db: Session = Depends(get_session)):
settings_manager = get_settings_manager()
if settings_manager.settings.AUTO_LOGIN:
user_id = UUID("3fa85f64-5717-4562-b3fc-2c963f66afa6")
return create_user_longterm_token(user_id, db)
return create_user_longterm_token(db)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,

View file

@ -115,14 +115,13 @@ def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
"""
new_user = User(
username="superuser",
password="12345",
password=get_password_hash("12345"),
is_active=True,
is_superuser=True,
last_login_at=None,
)
try:
new_user.password = get_password_hash(new_user.password)
db.add(new_user)
db.commit()
db.refresh(new_user)

View file

@ -30,6 +30,7 @@ def create_cache_folder(func):
def memoize_dict(maxsize=128):
cache = OrderedDict()
hash_to_key = {} # Mapping from hash to cache key
def decorator(func):
@functools.wraps(func)
@ -39,16 +40,29 @@ def memoize_dict(maxsize=128):
if key not in cache:
result = func(*args, **kwargs)
cache[key] = result
hash_to_key[hashed] = key # Store the mapping
if len(cache) > maxsize:
cache.popitem(last=False)
oldest_key = next(iter(cache))
oldest_hash = oldest_key[1]
del cache[oldest_key]
del hash_to_key[oldest_hash]
else:
result = cache[key]
wrapper.session_id = hashed # Store hash in the wrapper
return result
def clear_cache():
cache.clear()
hash_to_key.clear()
def get_result_by_session_id(session_id):
key = hash_to_key.get(session_id)
return cache.get(key) if key is not None else None
wrapper.clear_cache = clear_cache # type: ignore
wrapper.get_result_by_session_id = get_result_by_session_id # type: ignore
wrapper.hash = None
wrapper.cache = cache # type: ignore
return wrapper

View file

@ -18,7 +18,8 @@ class ServiceManager:
"""
Registers a new factory.
"""
self.factories[service_factory.service_class.name] = service_factory
if service_factory.service_class.name not in self.factories:
self.factories[service_factory.service_class.name] = service_factory
def get(self, service_name: ServiceType):
"""
@ -85,3 +86,12 @@ def initialize_services():
service_manager.register_factory(database_factory.DatabaseManagerFactory())
service_manager.register_factory(cache_factory.CacheManagerFactory())
service_manager.register_factory(chat_factory.ChatManagerFactory())
def initialize_settings_manager():
"""
Initialize the settings manager.
"""
from langflow.services.settings import factory as settings_factory
service_manager.register_factory(settings_factory.SettingsManagerFactory())

View file

@ -1,6 +1,7 @@
import contextlib
import json
import os
from shutil import copy2
import secrets
from typing import Optional, List
from pathlib import Path
@ -9,7 +10,8 @@ import yaml
from pydantic import BaseSettings, root_validator, validator
from langflow.utils.logger import logger
BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components")
# BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components")
BASE_COMPONENTS_PATH = str(Path(__file__).parent.parent.parent / "components")
class Settings(BaseSettings):
@ -30,6 +32,9 @@ class Settings(BaseSettings):
OUTPUT_PARSERS: dict = {}
CUSTOM_COMPONENTS: dict = {}
# Define the default LANGFLOW_DIR
CONFIG_DIR: Optional[str] = None
DEV: bool = False
DATABASE_URL: Optional[str] = None
CACHE: str = "InMemoryCache"
@ -41,12 +46,44 @@ class Settings(BaseSettings):
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_MINUTES: int = 70
# API Key to execute /process endpoint
API_KEY_SECRET_KEY: Optional[
str
] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a"
API_KEY_ALGORITHM: str = "HS256"
# If AUTO_LOGIN = True
# > The application does not request login and logs in automatically as a super user.
AUTO_LOGIN: bool = True
FIRST_SUPERUSER: str = "langflow"
FIRST_SUPERUSER_PASSWORD: str = "langflow"
@validator("CONFIG_DIR", pre=True, allow_reuse=True)
def set_langflow_dir(cls, value):
if not value:
import appdirs
# Define the app name and author
app_name = "langflow"
app_author = "logspace"
# Get the cache directory for the application
cache_dir = appdirs.user_cache_dir(app_name, app_author)
# Create a .langflow directory inside the cache directory
value = Path(cache_dir)
value.mkdir(parents=True, exist_ok=True)
if isinstance(value, str):
value = Path(value)
if not value.exists():
value.mkdir(parents=True, exist_ok=True)
return str(value)
@validator("DATABASE_URL", pre=True)
def set_database_url(cls, value):
def set_database_url(cls, value, values):
if not value:
logger.debug(
"No database_url provided, trying LANGFLOW_DATABASE_URL env variable"
@ -56,7 +93,28 @@ class Settings(BaseSettings):
logger.debug("Using LANGFLOW_DATABASE_URL env variable.")
else:
logger.debug("No DATABASE_URL env variable, using sqlite database")
value = "sqlite:///./langflow.db"
# 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"]:
raise ValueError(
"CONFIG_DIR not set, please set it or provide a DATABASE_URL"
)
new_path = f"{values['CONFIG_DIR']}/langflow.db"
if Path("./langflow.db").exists():
if Path(new_path).exists():
logger.debug(f"Database already exists at {new_path}, using it")
else:
try:
logger.debug("Copying existing database to new location")
copy2("./langflow.db", new_path)
logger.debug(f"Copied existing database to {new_path}")
except Exception:
logger.error("Failed to copy database, using default path")
new_path = "./langflow.db"
value = f"sqlite:///{new_path}"
return value
@ -139,12 +197,17 @@ class Settings(BaseSettings):
value = json.loads(str(value))
if isinstance(value, list):
for item in value:
if isinstance(item, Path):
item = str(item)
if item not in getattr(self, key):
getattr(self, key).append(item)
logger.debug(f"Extended {key}")
else:
getattr(self, key).append(value)
logger.debug(f"Appended {key}")
if isinstance(value, Path):
value = str(value)
if value not in getattr(self, key):
getattr(self, key).append(value)
logger.debug(f"Appended {key}")
else:
setattr(self, key, value)

File diff suppressed because it is too large Load diff

View file

@ -40,6 +40,7 @@
"esbuild": "^0.17.18",
"lodash": "^4.17.21",
"lucide-react": "^0.233.0",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
@ -71,7 +72,8 @@
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
},
"eslintConfig": {
"extends": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
import _ from "lodash";
import { useContext, useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import "reactflow/dist/style.css";
import "./App.css";
@ -9,10 +9,12 @@ import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
import CrashErrorComponent from "./components/CrashErrorComponent";
import Header from "./components/headerComponent";
import LoadingComponent from "./components/loadingComponent";
import { alertContext } from "./contexts/alertContext";
import { AuthContext } from "./contexts/authContext";
import { locationContext } from "./contexts/locationContext";
import { TabsContext } from "./contexts/tabsContext";
import { autoLogin, getLoggedUser } from "./controllers/API";
import Router from "./routes";
export default function App() {
@ -25,6 +27,7 @@ export default function App() {
setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext);
const {
errorData,
errorOpen,
@ -35,7 +38,11 @@ export default function App() {
successData,
successOpen,
setSuccessOpen,
setErrorData,
loading,
setLoading
} = useContext(alertContext);
const navigate = useNavigate();
// Initialize state variable for the list of alerts
const [alertsList, setAlertsList] = useState<
@ -49,6 +56,7 @@ export default function App() {
const isLoginPage = location.pathname.includes("login");
const isAdminPage = location.pathname.includes("admin");
const isSignUpPage = location.pathname.includes("signup");
const isLocalHost = window.location.href.includes("localhost");
// Use effect hook to update alertsList when a new alert is added
useEffect(() => {
@ -125,6 +133,37 @@ export default function App() {
);
};
//this function is to get the user logged in when the page is refreshed
const { setUserData, getAuthentication, login, setAutoLogin, logout } =
useContext(AuthContext);
useEffect(() => {
setTimeout(() => {
autoLogin().then((user) => {
if(user && user['access_token']){
user['refresh_token'] = "auto";
login(user['access_token'], user['refresh_token']);
setUserData(user);
setAutoLogin(true);
setLoading(false);
}
}).catch((error) => {
setAutoLogin(false);
if (getAuthentication() && !isLoginPage) {
getLoggedUser()
.then((user) => {
setUserData(user);
setLoading(false);
})
.catch((error) => {});
}
else{
setLoading(false);
}
});
}, 500);
}, []);
return (
//need parent component with width and height
<div className="flex h-full flex-col">
@ -137,8 +176,15 @@ export default function App() {
}}
FallbackComponent={CrashErrorComponent}
>
{!isLoginPage && !isSignUpPage && <Header />}
<Router />
{loading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
</ErrorBoundary>
<div></div>
<div className="app-div" style={{ zIndex: 999 }}>

View file

@ -111,6 +111,7 @@ export default function ParameterComponent({
let groupedObj = groupByFamily(myData, tooltipTitle!, left, flow!);
if (groupedObj && groupedObj.length > 0) {
//@ts-ignore
//@ts-ignore
refHtml.current = groupedObj.map((item, index) => {
const Icon: any =

View file

@ -8,6 +8,7 @@ import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { cleanEdges } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
@ -20,13 +21,14 @@ export default function GenericNode({
}: {
data: NodeDataType;
selected: boolean;
}) {
}): JSX.Element {
const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(TabsContext);
const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance } = useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [validationStatus, setValidationStatus] = useState(null);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
// State for outline color
const { sseData, isBuilding } = useSSE();
useEffect(() => {
@ -41,7 +43,7 @@ export default function GenericNode({
nodes: flow.data.nodes,
},
updateEdge: (edge) => {
flow.data.edges = edge;
flow.data!.edges = edge;
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
@ -77,7 +79,7 @@ export default function GenericNode({
"generic-node-div"
)}
>
{data.node.beta && (
{data.node?.beta && (
<div className="beta-badge-wrapper">
<div className="beta-badge-content">BETA</div>
</div>
@ -90,9 +92,9 @@ export default function GenericNode({
iconColor={`${nodeColors[types[data.type]]}`}
/>
<div className="generic-node-tooltip-div">
<ShadTooltip content={data.node.display_name}>
<ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary">
{data.node.display_name}
{data.node?.display_name}
</div>
</ShadTooltip>
</div>
@ -117,7 +119,9 @@ export default function GenericNode({
{typeof validationStatus.params === "string"
? validationStatus.params
.split("\n")
.map((line, index) => <div key={index}>{line}</div>)
.map((line: string, index: number) => (
<div key={index}>{line}</div>
))
: ""}
</div>
)
@ -155,20 +159,20 @@ export default function GenericNode({
</div>
<div className="generic-node-desc">
<div className="generic-node-desc-text">{data.node.description}</div>
<div className="generic-node-desc-text">{data.node?.description}</div>
<>
{Object.keys(data.node.template)
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map((templateField: string, idx) => (
<div key={idx}>
{data.node.template[templateField].show &&
!data.node.template[templateField].advanced ? (
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
key={
(data.node.template[templateField].input_types?.join(
(data.node!.template[templateField].input_types?.join(
";"
) ?? data.node.template[templateField].type) +
) ?? data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
@ -178,39 +182,39 @@ export default function GenericNode({
setData={setData}
color={
nodeColors[
types[data.node.template[templateField].type]
types[data.node?.template[templateField].type!]
] ??
nodeColors[data.node.template[templateField].type] ??
nodeColors[data.node?.template[templateField].type!] ??
nodeColors.unknown
}
title={
data.node.template[templateField].display_name
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node.template[templateField].name
: data.node?.template[templateField].name
? toTitleCase(data.node.template[templateField].name)
: toTitleCase(templateField)
}
info={data.node.template[templateField].info}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node.template[templateField].input_types?.join(
data.node?.template[templateField].input_types?.join(
"\n"
) ?? data.node.template[templateField].type
) ?? data.node?.template[templateField].type
}
required={data.node.template[templateField].required}
required={data.node?.template[templateField].required}
id={
(data.node.template[templateField].input_types?.join(
(data.node?.template[templateField].input_types?.join(
";"
) ?? data.node.template[templateField].type) +
) ?? data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
left={true}
type={data.node.template[templateField].type}
type={data.node?.template[templateField].type}
optionalHandle={
data.node.template[templateField].input_types
data.node?.template[templateField].input_types
}
/>
) : (
@ -220,25 +224,25 @@ export default function GenericNode({
))}
<div
className={classNames(
Object.keys(data.node.template).length < 1 ? "hidden" : "",
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center"
)}
>
{" "}
</div>
<ParameterComponent
key={[data.type, data.id, ...data.node.base_classes].join("|")}
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node.output_types && data.node.output_types.length > 0
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node.base_classes.join("\n")}
id={[data.type, data.id, ...data.node.base_classes].join("|")}
type={data.node.base_classes.join("|")}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
type={data.node?.base_classes.join("|")}
left={false}
/>
</>

View file

@ -7,7 +7,7 @@ import { SingleAlertComponentType } from "../../../../types/alerts";
export default function SingleAlert({
dropItem,
removeAlert,
}: SingleAlertComponentType) {
}: SingleAlertComponentType): JSX.Element {
const [show, setShow] = useState(true);
const type = dropItem.type;

View file

@ -9,7 +9,9 @@ import { alertContext } from "../../contexts/alertContext";
import { AlertDropdownType } from "../../types/alerts";
import SingleAlert from "./components/singleAlertComponent";
export default function AlertDropdown({ children }: AlertDropdownType) {
export default function AlertDropdown({
children,
}: AlertDropdownType): JSX.Element {
const {
notificationList,
clearNotificationList,

View file

@ -8,7 +8,7 @@ export default function ErrorAlert({
list = [],
id,
removeAlert,
}: ErrorAlertType) {
}: ErrorAlertType): JSX.Element {
const [show, setShow] = useState(true);
useEffect(() => {
if (show) {

View file

@ -2,7 +2,7 @@ import { useEffect } from "react";
export function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
const listener = (event: Event) => {
// Do nothing if clicking ref's element or its children
if (!ref.current || ref.current.contains(event.target)) {
return;

View file

@ -9,7 +9,7 @@ export default function NoticeAlert({
link = "",
id,
removeAlert,
}: NoticeAlertType) {
}: NoticeAlertType): JSX.Element {
const [show, setShow] = useState(true);
useEffect(() => {
if (show) {

View file

@ -7,7 +7,7 @@ export default function SuccessAlert({
title,
id,
removeAlert,
}: SuccessAlertType) {
}: SuccessAlertType): JSX.Element {
const [show, setShow] = useState(true);
useEffect(() => {
if (show) {

View file

@ -12,12 +12,12 @@ export default function AccordionComponent({
children,
open = [],
keyValue,
}: AccordionComponentType) {
}: AccordionComponentType): JSX.Element {
const [value, setValue] = useState(
open.length === 0 ? "" : getOpenAccordion()
);
function getOpenAccordion() {
function getOpenAccordion(): string {
let value = "";
open.forEach((el) => {
if (el == trigger) {
@ -28,10 +28,17 @@ export default function AccordionComponent({
return value;
}
function handleClick() {
value === "" ? setValue(keyValue) : setValue("");
function handleClick(): void {
value === "" ? setValue(keyValue!) : setValue("");
}
const handleKeyDown = (event) => {
if (event.key === "Backspace") {
event.preventDefault();
event.stopPropagation();
}
};
return (
<>
<Accordion
@ -39,8 +46,9 @@ export default function AccordionComponent({
className="w-full"
value={value}
onValueChange={setValue}
onKeyDown={handleKeyDown}
>
<AccordionItem value={keyValue} className="border-b">
<AccordionItem value={keyValue!} className="border-b">
<AccordionTrigger
onClick={() => {
handleClick();

View file

@ -1,4 +1,9 @@
export default function CrashErrorComponent({ error, resetErrorBoundary }) {
import { crashComponentPropsType } from "../../types/components";
export default function CrashErrorComponent({
error,
resetErrorBoundary,
}: crashComponentPropsType): JSX.Element {
return (
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-foreground bg-opacity-50">
<div className="flex h-1/3 min-h-fit max-w-4xl flex-col justify-evenly rounded-lg bg-background p-8 text-start shadow-lg">

View file

@ -3,19 +3,8 @@ import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea";
import { readFlowsFromDatabase } from "../../controllers/API";
type InputProps = {
name: string | null;
description: string | null;
maxLength?: number;
flows: Array<{ id: string; name: string; description: string }>;
tabId: string;
invalidName: boolean;
setInvalidName: (invalidName: boolean) => void;
setName: (name: string) => void;
setDescription: (description: string) => void;
updateFlow: (flow: { id: string; name: string }) => void;
};
import { InputProps } from "../../types/components";
import { FlowType } from "../../types/flow";
export const EditFlowSettings: React.FC<InputProps> = ({
name,
@ -27,13 +16,12 @@ export const EditFlowSettings: React.FC<InputProps> = ({
tabId,
setName,
setDescription,
updateFlow,
}) => {
}: InputProps): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const nameLists = useRef([]);
const nameLists = useRef<string[]>([]);
useEffect(() => {
readFlowsFromDatabase().then((flows) => {
flows.forEach((flow) => {
flows.forEach((flow: FlowType) => {
nameLists.current.push(flow.name);
});
});
@ -46,21 +34,34 @@ export const EditFlowSettings: React.FC<InputProps> = ({
} else {
setIsMaxLength(false);
}
if (invalidName !== undefined) {
if (!nameLists.current.includes(value)) {
setInvalidName(false);
} else {
setInvalidName(true);
}
}
if (!nameLists.current.includes(value)) {
setInvalidName(false);
setInvalidName!(false);
} else {
setInvalidName(true);
setInvalidName!(true);
}
setName(value);
setCurrentName(value);
};
const [desc, setDesc] = useState(
flows.find((flow) => flow.id === tabId).description
);
const [currentName, setCurrentName] = useState(name);
const [currentDescription, setCurrentDescription] = useState(description);
useEffect(() => {
setCurrentName(name);
setCurrentDescription(description);
}, [name, description]);
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
flows.find((flow) => flow.id === tabId).description = event.target.value;
setDesc(flows.find((flow) => flow.id === tabId).description);
flows.find((f) => f.id === tabId).description = event.target.value;
setCurrentDescription(flows.find((f) => f.id === tabId).description);
setDescription(event.target.value);
};
@ -81,7 +82,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
value={currentName ?? ""}
placeholder="File name"
id="name"
maxLength={maxLength}
@ -96,7 +97,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
name="description"
id="description"
onChange={handleDescriptionChange}
value={desc}
value={currentDescription}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}

View file

@ -12,17 +12,17 @@ import { Button } from "../ui/button";
export default function PaginatorComponent({
pageSize = 10,
pageIndex = 1,
rowsCount = [10, 20, 30],
pageIndex = 0,
rowsCount = [10, 20, 50, 100],
totalRowsCount = 0,
paginate,
}: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize);
const [index, setPageIndex] = useState(pageIndex);
const [maxIndex, setMaxPageIndex] = useState(
Math.ceil(totalRowsCount / pageSize)
);
const [currentPage, setCurrentPage] = useState(1);
return (
<>
@ -35,7 +35,7 @@ export default function PaginatorComponent({
onValueChange={(pageSize: string) => {
setPageSize(Number(pageSize));
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
paginate(Number(pageSize), index);
paginate(Number(pageSize), 0);
}}
>
<SelectTrigger className="w-[100px]">
@ -51,30 +51,30 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {index} of {maxIndex}
Page {currentPage} of {maxIndex}
</div>
<div className="flex items-center space-x-2">
<Button
disabled={index <= 0}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(1);
paginate(size, 1);
setPageIndex(0);
setCurrentPage(1);
paginate(size, 0);
}}
>
<span className="sr-only">Go to first page</span>
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
</Button>
<Button
disabled={index <= 0}
onClick={() => {
if (index <= 1) {
setPageIndex(1);
paginate(size, 1);
} else {
{
setPageIndex(index - 1);
paginate(size, index - 1);
}
if (index > 0) {
const pgIndex = size - index;
setCurrentPage(currentPage - 1);
setPageIndex(pgIndex);
paginate(size, pgIndex);
}
}}
variant="outline"
@ -84,14 +84,12 @@ export default function PaginatorComponent({
<IconComponent name="ChevronLeft" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
onClick={() => {
if (index >= maxIndex) {
setPageIndex(maxIndex);
paginate(size, maxIndex);
} else {
setPageIndex(index + 1);
paginate(size, index + 1);
}
const pgIndex = size + index;
setPageIndex(pgIndex);
setCurrentPage(currentPage + 1);
paginate(size, pgIndex);
}}
variant="outline"
className="h-8 w-8 p-0"
@ -100,11 +98,13 @@ export default function PaginatorComponent({
<IconComponent name="ChevronRight" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(maxIndex);
paginate(size, maxIndex);
setPageIndex(maxIndex - 1);
setCurrentPage(maxIndex);
paginate(size, size);
}}
>
<span className="sr-only">Go to last page</span>

View file

@ -3,16 +3,16 @@ import { RadialProgressType } from "../../types/components";
export default function RadialProgressComponent({
value,
color,
}: RadialProgressType) {
}: RadialProgressType): JSX.Element {
const style = {
"--value": value * 100,
"--value": value! * 100,
"--size": "1.5rem",
"--thickness": "2px",
} as React.CSSProperties;
return (
<div className={"radial-progress " + color} style={style}>
<strong className="text-[8px]">{Math.trunc(value * 100)}%</strong>
<strong className="text-[8px]">{Math.trunc(value! * 100)}%</strong>
</div>
);
}

View file

@ -3,20 +3,9 @@ import type { FC } from "react";
import React from "react";
import { Tooltip as ReactTooltip } from "react-tooltip";
import "react-tooltip/dist/react-tooltip.css";
import { TooltipProps } from "../../types/components";
import { classNames } from "../../utils/utils";
type TooltipProps = {
selector: string;
content?: string;
disabled?: boolean;
htmlContent?: React.ReactNode;
className?: string; // This should use !impornant to override the default styles eg: '!bg-white'
position?: "top" | "right" | "bottom" | "left";
clickable?: boolean;
children: React.ReactNode;
delayShow?: number;
};
const TooltipReact: FC<TooltipProps> = ({
selector,
content,
@ -27,7 +16,7 @@ const TooltipReact: FC<TooltipProps> = ({
className,
clickable,
delayShow,
}) => {
}: TooltipProps): JSX.Element => {
return (
<div className="tooltip-container">
{React.cloneElement(children as React.ReactElement, {
@ -38,7 +27,7 @@ const TooltipReact: FC<TooltipProps> = ({
content={content}
className={classNames(
"z-[9999] !bg-white !text-xs !font-normal !text-foreground !opacity-100 !shadow-md",
className
className!
)}
place={position}
clickable={clickable}

View file

@ -1,11 +1,12 @@
import DOMPurify from "dompurify";
import { SanitizedHTMLWrapperType } from "../../types/components";
const SanitizedHTMLWrapper = ({
className,
content,
onClick,
suppressWarning = false,
}) => {
}: SanitizedHTMLWrapperType): JSX.Element => {
const sanitizedHTML = DOMPurify.sanitize(content);
return (

View file

@ -8,7 +8,7 @@ export default function ShadTooltip({
children,
styleClasses,
delayDuration = 500,
}: ShadToolTipType) {
}: ShadToolTipType): JSX.Element {
return (
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>

View file

@ -5,7 +5,7 @@ export default function Tooltip({
children,
title,
placement,
}: TooltipComponentType) {
}: TooltipComponentType): JSX.Element {
return (
<LightTooltip placement={placement} title={title} arrow>
{children}

View file

@ -0,0 +1,24 @@
import { useContext, useEffect } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "../../contexts/authContext";
export const ProtectedAdminRoute = ({ children }) => {
const { isAdmin, isAuthenticated, logout, getAuthentication, userData, autoLogin } =
useContext(AuthContext);
useEffect(() => {
if (!isAuthenticated && !getAuthentication()) {
window.location.replace("/login");
logout();
}
}, [isAuthenticated, getAuthentication, logout, userData]);
if (!isAuthenticated && !getAuthentication()) {
return <Navigate to="/login" replace />;
}
if (userData && !isAdmin || autoLogin) {
return <Navigate to="/" replace />;
}
return children;
};

View file

@ -0,0 +1,14 @@
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "../../contexts/authContext";
export const ProtectedRoute = ({ children }) => {
const { isAuthenticated, logout, getAuthentication } =
useContext(AuthContext);
if (!isAuthenticated && !getAuthentication()) {
logout();
return <Navigate to="/login" replace />;
}
return children;
};

View file

@ -0,0 +1,19 @@
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "../../contexts/authContext";
export const ProtectedLoginRoute = ({ children }) => {
const { getAuthentication, autoLogin } = useContext(AuthContext);
if (autoLogin === true) {
window.location.replace("/");
return <Navigate to="/" replace />;
}
if (getAuthentication()) {
window.location.replace("/");
return <Navigate to="/" replace />;
}
return children;
};

View file

@ -1,6 +1,6 @@
import { useContext } from "react";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowType } from "../../types/flow";
import { cardComponentPropsType } from "../../types/components";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
import {
@ -16,12 +16,7 @@ export const CardComponent = ({
id,
onDelete,
button,
}: {
flow: FlowType;
id: string;
onDelete?: () => void;
button?: JSX.Element;
}) => {
}: cardComponentPropsType): JSX.Element => {
const { removeFlow } = useContext(TabsContext);
return (

View file

@ -0,0 +1,13 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
export const CatchAllRoute = () => {
const navigate = useNavigate();
// Redirect to the root ("/") when the catch-all route is matched
useEffect(() => {
navigate("/");
}, []);
return null;
};

View file

@ -8,6 +8,8 @@ import { postBuildInit } from "../../../controllers/API";
import { FlowType } from "../../../types/flow";
import { TabsContext } from "../../../contexts/tabsContext";
import { parsedDataType } from "../../../types/components";
import { TabsState } from "../../../types/tabs";
import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent";
@ -21,7 +23,7 @@ export default function BuildTrigger({
flow: FlowType;
setIsBuilt: any;
isBuilt: boolean;
}) {
}): JSX.Element {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(TabsContext);
@ -30,12 +32,12 @@ export default function BuildTrigger({
const eventClick = isBuilding ? "pointer-events-none" : "";
const [progress, setProgress] = useState(0);
async function handleBuild(flow: FlowType) {
async function handleBuild(flow: FlowType): Promise<void> {
try {
if (isBuilding) {
return;
}
const errors = validateNodes(reactFlowInstance);
const errors = validateNodes(reactFlowInstance!);
if (errors.length > 0) {
setErrorData({
title: "Oops! Looks like you missed something",
@ -69,7 +71,7 @@ export default function BuildTrigger({
const response = await postBuildInit(flow);
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults = [];
let validationResults: boolean[] = [];
let finished = false;
const apiUrl = `/api/v1/build/stream/${flowId}`;
const eventSource = new EventSource(apiUrl);
@ -91,7 +93,8 @@ export default function BuildTrigger({
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
setTabsState((old) => {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
@ -120,13 +123,13 @@ export default function BuildTrigger({
// Step 3: Wait for the stream to finish
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = validationResults.length === flow.data.nodes.length;
finished = validationResults.length === flow.data!.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
}
function processStreamResult(parsedData) {
function processStreamResult(parsedData: parsedDataType) {
// Process each chunk of data here
// Parse the chunk and update the context
try {

View file

@ -8,12 +8,18 @@ import {
FLOW_NOT_BUILT_TITLE,
} from "../../../constants/constants";
import { alertContext } from "../../../contexts/alertContext";
import { chatTriggerPropType } from "../../../types/components";
import IconComponent from "../../genericIconComponent";
export default function ChatTrigger({ open, setOpen, isBuilt, canOpen }) {
export default function ChatTrigger({
open,
setOpen,
isBuilt,
canOpen,
}: chatTriggerPropType): JSX.Element {
const { setErrorData } = useContext(alertContext);
function handleClick() {
function handleClick(): void {
if (isBuilt) {
if (canOpen) {
setOpen(true);

View file

@ -10,7 +10,7 @@ import { getBuildStatus } from "../../controllers/API";
import FormModal from "../../modals/formModal";
import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType) {
export default function Chat({ flow }: ChatType): JSX.Element {
const [open, setOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
@ -44,11 +44,11 @@ export default function Chat({ flow }: ChatType) {
}, [flow]);
const prevNodesRef = useRef<any[] | undefined>();
const nodes = useNodes();
const nodes: NodeType[] = useNodes();
useEffect(() => {
const prevNodes = prevNodesRef.current;
const currentNodes = nodes.map((node: NodeType) =>
_.cloneDeep(node.data.node.template)
_.cloneDeep(node.data.node?.template)
);
if (
tabsState &&

View file

@ -33,7 +33,7 @@ export default function CodeAreaComponent({
dynamic={dynamic}
value={myValue}
nodeClass={nodeClass}
setNodeClass={setNodeClass}
setNodeClass={setNodeClass!}
setValue={(value: string) => {
setMyValue(value);
onChange(value);

View file

@ -29,7 +29,7 @@ import {
TabsTrigger,
} from "../../components/ui/tabs";
import { darkContext } from "../../contexts/darkContext";
import { FlowType } from "../../types/flow/index";
import { codeTabsPropsType } from "../../types/components";
import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
@ -40,28 +40,15 @@ export default function CodeTabsComponent({
setActiveTab,
isMessage,
tweaks,
}: {
flow?: FlowType;
tabs: any;
activeTab: string;
setActiveTab: any;
isMessage?: boolean;
tweaks?: {
tweak?: any;
tweaksList?: any;
buildContent?: any;
getValue?: any;
buildTweakObject?: any;
};
}) {
}: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const [data, setData] = useState(flow ? flow["data"]["nodes"] : null);
const [openAccordion, setOpenAccordion] = useState([]);
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
const { dark } = useContext(darkContext);
useEffect(() => {
if (flow && flow["data"]["nodes"]) {
setData(flow["data"]["nodes"]);
if (flow && flow["data"]!["nodes"]) {
setData(flow["data"]!["nodes"]);
}
}, [flow]);
@ -102,8 +89,8 @@ export default function CodeTabsComponent({
};
function openAccordions() {
let accordionsToOpen = [];
tweaks.tweak.current.forEach((el) => {
let accordionsToOpen: string[] = [];
tweaks?.tweak!.current.forEach((el) => {
Object.keys(el).forEach((key) => {
if (Object.keys(el[key]).length > 0) {
accordionsToOpen.push(key);
@ -205,9 +192,9 @@ export default function CodeTabsComponent({
: "overflow-hidden"
)}
>
{data.map((node: any, index) => (
{data?.map((node: any, index) => (
<div className="px-3" key={index}>
{tweaks.tweaksList.current.includes(
{tweaks?.tweaksList!.current.includes(
node["data"]["id"]
) && (
<AccordionComponent
@ -290,14 +277,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -310,7 +297,7 @@ export default function CodeTabsComponent({
templateField
].multiline ? (
<ShadTooltip
content={tweaks.buildContent(
content={tweaks.buildContent!(
node.data.node.template[
templateField
].value
@ -339,14 +326,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node
@ -384,14 +371,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -417,14 +404,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = e;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
e,
node.data.node.template[
@ -440,7 +427,7 @@ export default function CodeTabsComponent({
templateField
].type === "file" ? (
<ShadTooltip
content={tweaks.buildContent(
content={tweaks.buildContent!(
!node.data.node.template[
templateField
].value ||
@ -508,14 +495,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -544,14 +531,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -596,14 +583,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -617,7 +604,7 @@ export default function CodeTabsComponent({
templateField
].type === "prompt" ? (
<ShadTooltip
content={tweaks.buildContent(
content={tweaks.buildContent!(
!node.data.node.template[
templateField
].value ||
@ -651,14 +638,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -673,8 +660,8 @@ export default function CodeTabsComponent({
templateField
].type === "code" ? (
<ShadTooltip
content={tweaks.buildContent(
tweaks.getValue(
content={tweaks.buildContent!(
tweaks.getValue!(
node.data.node.template[
templateField
].value,
@ -706,14 +693,14 @@ export default function CodeTabsComponent({
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList[
newInputList![
index
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject(
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -742,7 +729,7 @@ export default function CodeTabsComponent({
</AccordionComponent>
)}
{tweaks.tweaksList.current.length === 0 && (
{tweaks?.tweaksList!.current.length === 0 && (
<>
<div className="pt-3">
No tweaks are available for this flow.

View file

@ -11,7 +11,7 @@ export default function Dropdown({
editNode = false,
numberOfOptions = 0,
apiModal = false,
}: DropDownComponentType) {
}: DropDownComponentType): JSX.Element {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
);

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { Input } from "../ui/input";
export default function FloatComponent({
@ -7,7 +8,7 @@ export default function FloatComponent({
onChange,
disabled,
editNode = false,
}: FloatComponentType) {
}: FloatComponentType): JSX.Element {
const step = 0.1;
const min = 0;
const max = 1;
@ -43,6 +44,9 @@ export default function FloatComponent({
onChange={(event) => {
onChange(event.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "0");
}}
/>
</div>
);

View file

@ -1,17 +1,19 @@
import { forwardRef } from "react";
import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
export default function IconComponent({
name,
className,
iconColor,
}: IconComponentProps): JSX.Element {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
return (
<TargetIcon
strokeWidth={1.5}
className={className}
style={{ color: iconColor }}
/>
);
}
const ForwardedIconComponent = forwardRef(
({ name, className, iconColor }: IconComponentProps, ref) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
return (
<TargetIcon
strokeWidth={1.5}
className={className}
style={iconColor ? { color: iconColor } : {}}
ref={ref}
/>
);
}
);
export default ForwardedIconComponent;

View file

@ -12,10 +12,11 @@ import { Link, useNavigate } from "react-router-dom";
import { alertContext } from "../../../../contexts/alertContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import { menuBarPropsType } from "../../../../types/components";
import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({ flows, tabId }) => {
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
const { addFlow } = useContext(TabsContext);
const { setErrorData } = useContext(alertContext);
const { undo, redo } = useContext(undoRedoContext);
@ -25,12 +26,12 @@ export const MenuBar = ({ flows, tabId }) => {
function handleAddFlow() {
try {
addFlow(null, true).then((id) => {
addFlow(undefined, true).then((id) => {
navigate("/flow/" + id);
});
// saveFlowStyleInDataBase();
} catch (err) {
setErrorData(err);
setErrorData(err as { title: string; list?: Array<string> });
}
}
let current_flow = flows.find((flow) => flow.id === tabId);
@ -45,7 +46,9 @@ export const MenuBar = ({ flows, tabId }) => {
<DropdownMenuTrigger asChild>
<Button asChild variant="primary" size="sm">
<div className="header-menu-bar-display">
<div className="header-menu-flow-name">{current_flow.name}</div>
<div className="header-menu-flow-name">
{current_flow!.name}
</div>
<IconComponent name="ChevronDown" className="h-4 w-4" />
</div>
</Button>

View file

@ -1,9 +1,10 @@
import { useContext, useEffect, useState } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { Link, useLocation } from "react-router-dom";
import { Link, useLocation, useNavigate } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getRepoStars } from "../../controllers/API";
@ -12,31 +13,50 @@ import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar";
export default function Header() {
export default function Header(): JSX.Element {
const { flows, tabId } = useContext(TabsContext);
const { dark, setDark } = useContext(darkContext);
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const { logout, autoLogin, isAdmin, stars } = useContext(AuthContext);
const navigate = useNavigate();
const [stars, setStars] = useState(null);
// Get and set numbers of stars on header
useEffect(() => {
async function fetchStars() {
const starsCount = await getRepoStars("logspace-ai", "langflow");
setStars(starsCount);
}
fetchStars();
}, []);
return (
<div className="header-arrangement">
<div className="header-start-display">
<Link to="/">
<span className="ml-4 text-2xl"></span>
</Link>
<Button variant="outline" className="">
Sign out
</Button>
{location.pathname === "/admin" && (
<Button
onClick={() => {
navigate("/");
}}
variant="outline"
className=""
>
Main page
</Button>
)}
{autoLogin === false && (
<Button
onClick={() => {
logout();
navigate("/login");
}}
variant="outline"
className=""
>
Sign out
</Button>
)}
{isAdmin && !autoLogin && location.pathname !== "/admin" && (
<Button variant="outline" onClick={() => navigate("/admin")}>
Admin page
</Button>
)}
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
<MenuBar flows={flows} tabId={tabId} />
)}
@ -119,6 +139,18 @@ export default function Header() {
/>
</div>
</AlertDropdown>
{!autoLogin && (
<button
onClick={() => {
navigate("/account/api-keys");
}}
>
<IconComponent
name="Key"
className="side-bar-button-size text-muted-foreground hover:text-accent-foreground"
/>
</button>
)}
</div>
</div>
</div>

View file

@ -1,6 +1,7 @@
import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
@ -14,7 +15,7 @@ export default function InputComponent({
editNode = false,
placeholder = "Type something...",
className,
}: InputComponentType) {
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
// Clear component state
@ -39,12 +40,15 @@ export default function InputComponent({
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
className
className!
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
onChange(e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}
/>
</Form.Control>
) : (
@ -59,12 +63,15 @@ export default function InputComponent({
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
className
className!
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
onChange(e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}
/>
)}
{password && (
@ -74,7 +81,8 @@ export default function InputComponent({
? "input-component-true-button"
: "input-component-false-button"
)}
onClick={() => {
onClick={(event) => {
event.preventDefault();
setPwdVisible(!pwdVisible);
}}
>

View file

@ -13,7 +13,7 @@ export default function InputFileComponent({
fileTypes,
onFileChange,
editNode = false,
}: FileComponentType) {
}: FileComponentType): JSX.Element {
const [myValue, setMyValue] = useState(value);
const [loading, setLoading] = useState(false);
const { setErrorData } = useContext(alertContext);
@ -41,7 +41,7 @@ export default function InputFileComponent({
setMyValue(value);
}, [value]);
const handleButtonClick = () => {
const handleButtonClick = (): void => {
// Create a file input element
const input = document.createElement("input");
input.type = "file";
@ -49,7 +49,7 @@ export default function InputFileComponent({
input.style.display = "none"; // Hidden from view
input.multiple = false; // Allow only one file selection
input.onchange = (event: Event) => {
input.onchange = (event: Event): void => {
setLoading(true);
// Get the selected file

View file

@ -11,7 +11,7 @@ export default function InputListComponent({
onChange,
disabled,
editNode = false,
}: InputListComponentType) {
}: InputListComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange([""]);
@ -39,6 +39,12 @@ export default function InputListComponent({
newInputList[idx] = event.target.value;
onChange(newInputList);
}}
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Backspace") {
e.preventDefault();
e.stopPropagation();
}
}}
/>
{idx === value.length - 1 ? (
<button

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { Input } from "../ui/input";
export default function IntComponent({
@ -7,7 +8,7 @@ export default function IntComponent({
onChange,
disabled,
editNode = false,
}: FloatComponentType) {
}: FloatComponentType): JSX.Element {
const min = 0;
// Clear component state
@ -37,6 +38,7 @@ export default function IntComponent({
) {
event.preventDefault();
}
handleKeyDown(event, value, "0");
}}
type="number"
step="1"

View file

@ -1,8 +1,8 @@
type LoadingComponentProps = {
remSize: number;
};
import { LoadingComponentProps } from "../../types/components";
export default function LoadingComponent({ remSize }: LoadingComponentProps) {
export default function LoadingComponent({
remSize,
}: LoadingComponentProps): JSX.Element {
return (
<div role="status" className="m-auto w-min">
<svg

View file

@ -14,7 +14,7 @@ export default function PromptAreaComponent({
onChange,
disabled,
editNode = false,
}: TextAreaComponentType) {
}: TextAreaComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange("");
@ -23,9 +23,9 @@ export default function PromptAreaComponent({
useEffect(() => {
if (value !== "" && !editNode) {
postValidatePrompt(field_name, value, nodeClass).then((apiReturn) => {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass(apiReturn.data.frontend_node);
setNodeClass!(apiReturn.data.frontend_node);
// need to update reactFlowInstance to re-render the nodes.
}
});

View file

@ -10,7 +10,7 @@ export default function TextAreaComponent({
onChange,
disabled,
editNode = false,
}: TextAreaComponentType) {
}: TextAreaComponentType): JSX.Element {
// Clear text area
useEffect(() => {
if (disabled) {

View file

@ -7,7 +7,7 @@ export default function ToggleComponent({
enabled,
setEnabled,
disabled,
}: ToggleComponentType) {
}: ToggleComponentType): JSX.Element {
// set component state as disabled
useEffect(() => {
if (disabled) {

View file

@ -6,7 +6,7 @@ export default function ToggleShadComponent({
setEnabled,
disabled,
size,
}: ToggleComponentType) {
}: ToggleComponentType): JSX.Element {
let scaleX, scaleY;
switch (size) {
case "small":

View file

@ -23,14 +23,14 @@ export default function RenameLabel(props) {
});
if (inputRef.current) {
setTimeout(() => {
inputRef.current.focus();
inputRef.current?.focus();
}, 100);
}
}
resizeInput();
}, [isRename]);
const inputRef = useRef(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const resizeInput = () => {
const input = inputRef.current;

View file

@ -29,6 +29,7 @@ export const INVALID_CHARACTERS = [
*/
export const regexHighlight = /\{([^}]+)\}/g;
export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/;
export const programmingLanguages: languageMap = {
javascript: ".js",
@ -507,7 +508,11 @@ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
"http://localhost:7860/login",
];
export const skipNodeUpdate = ["CustomComponent"];
export const CONTROL_INPUT_STATE = {
password: "",
cnfPassword: "",
@ -518,6 +523,18 @@ export const CONTROL_LOGIN_STATE = {
username: "",
password: "",
};
export const CONTROL_NEW_USER = {
username: "",
password: "",
is_active: false,
is_superuser: false,
};
export const CONTROL_NEW_API_KEY = {
apikeyname: "",
};
export const tabsCode = [];
export function tabsArray(codes: string[], method: number) {
@ -598,3 +615,8 @@ export function tabsArray(codes: string[], method: number) {
},
];
}
export const BASE_URL_API = "http://localhost:7860/";
export const SIGN_UP_SUCCESS =
"Congratulations on signing up! Your account is currently awaiting activation by our admin team.";

View file

@ -1,34 +1,15 @@
import { createContext, ReactNode, useState } from "react";
import { AlertItemType } from "../types/alerts";
import { alertContextType } from "../types/typesContext";
import _ from "lodash";
//types for alertContextType
type alertContextType = {
errorData: { title: string; list?: Array<string> };
setErrorData: (newState: { title: string; list?: Array<string> }) => void;
errorOpen: boolean;
setErrorOpen: (newState: boolean) => void;
noticeData: { title: string; link?: string };
setNoticeData: (newState: { title: string; link?: string }) => void;
noticeOpen: boolean;
setNoticeOpen: (newState: boolean) => void;
successData: { title: string };
setSuccessData: (newState: { title: string }) => void;
successOpen: boolean;
setSuccessOpen: (newState: boolean) => void;
notificationCenter: boolean;
setNotificationCenter: (newState: boolean) => void;
notificationList: Array<AlertItemType>;
pushNotificationList: (Object: AlertItemType) => void;
clearNotificationList: () => void;
removeFromNotificationList: (index: string) => void;
};
//initial values to alertContextType
const initialValue: alertContextType = {
errorData: { title: "", list: [] },
setErrorData: () => {},
loading: true,
setLoading: () => {},
errorOpen: false,
setErrorOpen: () => {},
noticeData: { title: "", link: "" },
@ -55,6 +36,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
list?: Array<string>;
}>({ title: "", list: [] });
const [errorOpen, setErrorOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [noticeData, setNoticeDataState] = useState<{
title: string;
link?: string;
@ -65,7 +47,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
});
const [successOpen, setSuccessOpen] = useState(false);
const [notificationCenter, setNotificationCenter] = useState(false);
const [notificationList, setNotificationList] = useState([]);
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
const pushNotificationList = (notification: AlertItemType) => {
setNotificationList((old) => {
let newNotificationList = _.cloneDeep(old);
@ -141,6 +123,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
removeFromNotificationList,
clearNotificationList,
notificationList,
loading,
setLoading,
pushNotificationList,
setNotificationCenter,
notificationCenter,

View file

@ -1,44 +1,87 @@
import { createContext, useEffect, useState } from "react";
import { AuthContextType, userData } from "../types/contexts/auth";
import Cookies from "universal-cookie";
import { getLoggedUser, getRepoStars } from "../controllers/API";
import { Users } from "../types/api";
import { AuthContextType } from "../types/contexts/auth";
const initialValue: AuthContextType = {
isAdmin: false,
setIsAdmin: () => false,
isAuthenticated: false,
accessToken: null,
refreshToken: null,
login: () => {},
logout: () => {},
refreshAccessToken: () => Promise.resolve(),
userData: null,
setUserData: () => {},
getAuthentication: () => false,
authenticationErrorCount: 0,
autoLogin: false,
setAutoLogin: () => {},
stars: 0,
setStars: (stars) => 0,
};
const AuthContext = createContext<AuthContextType>(initialValue);
export const AuthContext = createContext<AuthContextType>(initialValue);
export function AuthProvider({ children }): React.ReactElement {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [userData, setUserData] = useState<userData>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [isAdmin, setIsAdmin] = useState<boolean>(false);
const [userData, setUserData] = useState<Users | null>(null);
const [autoLogin, setAutoLogin] = useState<boolean>(false);
const [stars, setStars] = useState<number>(0);
const cookies = new Cookies();
useEffect(() => {
const storedAccessToken = localStorage.getItem("access_token");
const storedAccessToken = cookies.get("access_token");
if (storedAccessToken) {
setAccessToken(storedAccessToken);
}
async function fetchStars() {
const starsCount = await getRepoStars("logspace-ai", "langflow");
setStars(starsCount);
}
fetchStars();
}, []);
useEffect(() => {
if (accessToken) {
getLoggedUser().then((user) => {
const isSuperUser = user.is_superuser;
setIsAdmin(isSuperUser);
});
}
}, [accessToken, isAdmin]);
function getAuthentication() {
const storedRefreshToken = cookies.get("refresh_token");
const storedAccess = cookies.get("access_token");
const auth = storedAccess && storedRefreshToken ? true : false;
return auth;
}
function login(newAccessToken: string, refreshToken: string) {
localStorage.setItem("access_token", newAccessToken);
cookies.set("access_token", newAccessToken, { path: "/" });
cookies.set("refresh_token", refreshToken, { path: "/" });
setAccessToken(newAccessToken);
// Store refreshToken if needed
setRefreshToken(refreshToken);
setIsAuthenticated(true);
}
function logout() {
localStorage.removeItem("access_token");
// Clear refreshToken if used
cookies.remove("access_token", { path: "/" });
cookies.remove("refresh_token", { path: "/" });
setUserData(null);
setAccessToken(null);
setRefreshToken(null);
setIsAuthenticated(false);
}
async function refreshAccessToken(refreshToken: string) {
try {
// Call your API to refresh the access token using the refresh token
const response = await fetch("/api/refresh-token", {
method: "POST",
headers: {
@ -50,6 +93,9 @@ export function AuthProvider({ children }): React.ReactElement {
if (response.ok) {
const data = await response.json();
login(data.accessToken, refreshToken);
getLoggedUser().then((user) => {
console.log("oi");
});
} else {
logout();
}
@ -62,13 +108,22 @@ export function AuthProvider({ children }): React.ReactElement {
// !! to convert string to boolean
<AuthContext.Provider
value={{
stars,
setStars,
isAdmin,
setIsAdmin,
isAuthenticated: !!accessToken,
accessToken,
refreshToken,
login,
logout,
refreshAccessToken,
setUserData,
userData,
getAuthentication,
authenticationErrorCount: 0,
setAutoLogin,
autoLogin,
}}
>
{children}

View file

@ -1,9 +1,5 @@
import { createContext, useEffect, useState } from "react";
type darkContextType = {
dark: {};
setDark: (newState: {}) => void;
};
import { darkContextType } from "../types/typesContext";
const initialValue = {
dark: {},
@ -14,13 +10,13 @@ export const darkContext = createContext<darkContextType>(initialValue);
export function DarkProvider({ children }) {
const [dark, setDark] = useState(
JSON.parse(window.localStorage.getItem("isDark")) ?? false
JSON.parse(window.localStorage.getItem("isDark")!) ?? false
);
useEffect(() => {
if (dark) {
document.getElementById("body").classList.add("dark");
document.getElementById("body")!.classList.add("dark");
} else {
document.getElementById("body").classList.remove("dark");
document.getElementById("body")!.classList.remove("dark");
}
window.localStorage.setItem("isDark", dark.toString());
}, [dark]);

View file

@ -3,6 +3,7 @@ import { ReactFlowProvider } from "reactflow";
import { TooltipProvider } from "../components/ui/tooltip";
import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext";
import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext";
@ -13,23 +14,25 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
//element to wrap all context
return (
<>
<TooltipProvider>
<ReactFlowProvider>
<DarkProvider>
<TypesProvider>
<LocationProvider>
<AlertProvider>
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
</SSEProvider>
</AlertProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</ReactFlowProvider>
</TooltipProvider>
<AuthProvider>
<TooltipProvider>
<ReactFlowProvider>
<DarkProvider>
<TypesProvider>
<LocationProvider>
<AlertProvider>
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
</SSEProvider>
</AlertProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</ReactFlowProvider>
</TooltipProvider>
</AuthProvider>
</>
);
}

View file

@ -1,34 +1,5 @@
import { createContext, ReactNode, useState } from "react";
//types for location context
type locationContextType = {
current: Array<string>;
setCurrent: (newState: Array<string>) => void;
isStackedOpen: boolean;
setIsStackedOpen: (newState: boolean) => void;
showSideBar: boolean;
setShowSideBar: (newState: boolean) => void;
extraNavigation: {
title: string;
options?: Array<{
name: string;
href: string;
icon: any;
children?: Array<any>;
}>;
};
setExtraNavigation: (newState: {
title: string;
options?: Array<{
name: string;
href: string;
icon: any;
children?: Array<any>;
}>;
}) => void;
extraComponent: any;
setExtraComponent: (newState: any) => void;
};
import { locationContextType } from "../types/typesContext";
//initial value for location context
const initialValue = {

View file

@ -7,8 +7,9 @@ import {
useRef,
useState,
} from "react";
import { addEdge } from "reactflow";
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow";
import ShortUniqueId from "short-unique-id";
import { skipNodeUpdate } from "../constants/constants";
import {
deleteFlowFromDatabase,
downloadFlowsFromDatabase,
@ -18,8 +19,9 @@ import {
uploadFlowsToDatabase,
} from "../controllers/API";
import { APIClassType, APITemplateType } from "../types/api";
import { FlowType, NodeType } from "../types/flow";
import { TabsContextType, TabsState } from "../types/tabs";
import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow";
import { TabsContextType, TabsState, errorsVarType } from "../types/tabs";
import {
addVersionToDuplicates,
updateIds,
@ -73,9 +75,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uid());
const { templates, reactFlowInstance } = useContext(typesContext);
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
const [lastCopiedSelection, setLastCopiedSelection] = useState<{
nodes: any;
edges: any;
} | null>(null);
const [tabsState, setTabsState] = useState<TabsState>({});
const [getTweak, setTweak] = useState([]);
const [getTweak, setTweak] = useState<tweakType>([]);
const newNodeId = useRef(uid());
function incrementNodeId() {
@ -130,8 +135,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
//get tabs from db
return readFlowsFromDatabase();
}
function processDBData(DbData) {
DbData.forEach((flow) => {
function processDBData(DbData: FlowType[]) {
DbData.forEach((flow: FlowType) => {
try {
if (!flow.data) {
return;
@ -144,7 +149,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
}
function processFlowEdges(flow) {
function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return;
flow.data.edges.forEach((edge) => {
edge.className = "";
@ -153,16 +158,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
function updateDisplay_name(node: NodeType, template: APIClassType) {
node.data.node.display_name = template["display_name"] || node.data.type;
node.data.node!.display_name = template["display_name"] || node.data.type;
}
function updateNodeDocumentation(node: NodeType, template: APIClassType) {
node.data.node.documentation = template["documentation"];
node.data.node!.documentation = template["documentation"];
}
function processFlowNodes(flow) {
function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => {
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
@ -180,7 +186,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
function updateNodeBaseClasses(node: NodeType, template: APIClassType) {
node.data.node.base_classes = template["base_classes"];
node.data.node!.base_classes = template["base_classes"];
}
function updateNodeEdges(
@ -188,10 +194,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
node: NodeType,
template: APIClassType
) {
flow.data.edges.forEach((edge) => {
flow.data!.edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle
.split("|")
?.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
@ -200,17 +206,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
function updateNodeDescription(node: NodeType, template: APIClassType) {
node.data.node.description = template["description"];
node.data.node!.description = template["description"];
}
function updateNodeTemplate(node: NodeType, template: APIClassType) {
node.data.node.template = updateTemplate(
node.data.node!.template = updateTemplate(
template["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType
node.data.node!.template as APITemplateType
);
}
function updateStateWithDbData(tabsData) {
function updateStateWithDbData(tabsData: FlowType[]) {
setFlows(tabsData);
}
@ -241,7 +247,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
link.download = `${
flowName && flowName != ""
? flowName
: flows.find((f) => f.id === tabId).name
: flows.find((f) => f.id === tabId)!.name
}.json`;
// simulate a click on the link element to trigger the download
@ -293,10 +299,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
input.onchange = (e: Event) => {
// check if the file type is application/json
if (
(e.target as HTMLInputElement).files[0].type === "application/json"
(e.target as HTMLInputElement).files![0].type === "application/json"
) {
// get the file from the file input
const currentfile = (e.target as HTMLInputElement).files[0];
const currentfile = (e.target as HTMLInputElement).files![0];
// read the file as text
currentfile.text().then((text) => {
// parse the text into a JSON object
@ -319,10 +325,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
input.onchange = (event: Event) => {
// check if the file type is application/json
if (
(event.target as HTMLInputElement).files[0].type === "application/json"
(event.target as HTMLInputElement).files![0].type === "application/json"
) {
// get the file from the file input
const file = (event.target as HTMLInputElement).files[0];
const file = (event.target as HTMLInputElement).files![0];
// read the file as text
const formData = new FormData();
formData.append("file", file);
@ -353,15 +359,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
*/
function paste(
selectionInstance,
selectionInstance: { nodes: Node[]; edges: Edge[] },
position: { x: number; y: number; paneX?: number; paneY?: number }
) {
let minimumX = Infinity;
let minimumY = Infinity;
let idsMap = {};
let nodes = reactFlowInstance.getNodes();
let edges = reactFlowInstance.getEdges();
selectionInstance.nodes.forEach((node) => {
let nodes: Node<NodeDataType>[] = reactFlowInstance!.getNodes();
let edges = reactFlowInstance!.getEdges();
selectionInstance.nodes.forEach((node: Node) => {
if (node.position.y < minimumY) {
minimumY = node.position.y;
}
@ -371,8 +377,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
const insidePosition = position.paneX
? { x: position.paneX + position.x, y: position.paneY + position.y }
: reactFlowInstance.project({ x: position.x, y: position.y });
? { x: position.paneX + position.x, y: position.paneY! + position.y }
: reactFlowInstance!.project({ x: position.x, y: position.y });
selectionInstance.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
@ -384,8 +390,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
id: newId,
type: "genericNode",
position: {
x: insidePosition.x + node.position.x - minimumX,
y: insidePosition.y + node.position.y - minimumY,
x: insidePosition.x + node.position!.x - minimumX,
y: insidePosition.y + node.position!.y - minimumY,
},
data: {
..._.cloneDeep(node.data),
@ -398,19 +404,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
.map((node) => ({ ...node, selected: false }))
.concat({ ...newNode, selected: false });
});
reactFlowInstance.setNodes(nodes);
reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge) => {
let source = idsMap[edge.source];
let target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle.split("|");
let sourceHandleSplitted = edge.sourceHandle!.split("|");
let sourceHandle =
sourceHandleSplitted[0] +
"|" +
source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = edge.targetHandle.split("|");
let targetHandleSplitted = edge.targetHandle!.split("|");
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
let id =
@ -438,21 +444,21 @@ export function TabsProvider({ children }: { children: ReactNode }) {
edges.map((edge) => ({ ...edge, selected: false }))
);
});
reactFlowInstance.setEdges(edges);
reactFlowInstance!.setEdges(edges);
}
const addFlow = async (
flow?: FlowType,
newProject?: Boolean
): Promise<String> => {
): Promise<String | undefined> => {
if (newProject) {
let flowData = extractDataFromFlow(flow);
let flowData = extractDataFromFlow(flow!);
if (flowData.description == "") {
flowData.description = getRandomDescription();
}
// Create a new flow with a default name if no flow is provided.
const newFlow = createNewFlow(flowData, flow);
const newFlow = createNewFlow(flowData, flow!);
processFlowEdges(newFlow);
processFlowNodes(newFlow);
@ -477,13 +483,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
} else {
paste(
{ nodes: flow.data.nodes, edges: flow.data.edges },
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
{ x: 10, y: 10 }
);
}
};
const extractDataFromFlow = (flow) => {
const extractDataFromFlow = (flow: FlowType) => {
let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
@ -496,18 +502,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return { data, description };
};
const updateEdges = (edges) => {
const updateEdges = (edges: Edge[]) => {
edges.forEach((edge) => {
edge.className =
(edge.targetHandle.split("|")[0] === "Text"
(edge.targetHandle!.split("|")[0] === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = edge.targetHandle.split("|")[0] === "Text";
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
});
};
const updateNodes = (nodes, edges) => {
const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => {
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
@ -517,8 +524,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
node.data.node.base_classes = template["base_classes"];
edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle
.split("|")
edge.sourceHandle = edge
.sourceHandle!.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
@ -533,14 +540,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
};
const createNewFlow = (flowData, flow) => ({
const createNewFlow = (
flowData: { data: ReactFlowJsonObject | null; description: string },
flow: FlowType
) => ({
description: flowData.description,
name: flow?.name ?? getRandomName(),
data: flowData.data,
id: "",
});
const addFlowToLocalState = (newFlow) => {
const addFlowToLocalState = (newFlow: FlowType) => {
setFlows((prevState) => {
return [...prevState, newFlow];
});
@ -591,7 +601,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
}
} catch (err) {
setErrorData(err);
setErrorData(err as errorsVarType);
}
}

View file

@ -1,14 +1,21 @@
import { createContext, ReactNode, useEffect, useState } from "react";
import { Node } from "reactflow";
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { Node, ReactFlowInstance } from "reactflow";
import { getAll } from "../controllers/API";
import { APIKindType } from "../types/api";
import { typesContextType } from "../types/typesContext";
import { alertContext } from "./alertContext";
//context to share types adn functions from nodes to flow
const initialValue: typesContextType = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
setReactFlowInstance: (newState: ReactFlowInstance) => {},
deleteNode: () => {},
types: {},
setTypes: () => {},
@ -22,13 +29,15 @@ export const typesContext = createContext<typesContextType>(initialValue);
export function TypesProvider({ children }: { children: ReactNode }) {
const [types, setTypes] = useState({});
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const [reactFlowInstance, setReactFlowInstance] =
useState<ReactFlowInstance | null>(null);
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
const { setLoading } = useContext(alertContext);
useEffect(() => {
let delay = 1000; // Start delay of 1 second
let intervalId = null;
let intervalId: NodeJS.Timer;
let retryCount = 0; // Count of retry attempts
const maxRetryCount = 5; // Max retry attempts
@ -40,6 +49,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
const result = await getAll();
// Make sure to only update the state if the component is still mounted.
if (isMounted) {
setLoading(false);
setData(result.data);
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
@ -69,7 +79,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
);
}
// Clear the interval if successful.
clearInterval(intervalId);
clearInterval(intervalId!);
} catch (error) {
console.error("An error has occurred while fetching types.");
}
@ -77,21 +87,20 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Start the initial interval.
intervalId = setInterval(getTypes, delay);
return () => {
// This will clear the interval when the component unmounts, or when the dependencies of the useEffect hook change.
clearInterval(intervalId);
clearInterval(intervalId!);
// Indicate that the component has been unmounted.
isMounted = false;
};
}, []);
function deleteNode(idx: string) {
reactFlowInstance.setNodes(
reactFlowInstance.getNodes().filter((node: Node) => node.id !== idx)
reactFlowInstance!.setNodes(
reactFlowInstance!.getNodes().filter((node: Node) => node.id !== idx)
);
reactFlowInstance.setEdges(
reactFlowInstance
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge) => edge.source !== idx && edge.target !== idx)
);

View file

@ -6,26 +6,15 @@ import {
useEffect,
useState,
} from "react";
import { Edge, Node, useReactFlow } from "reactflow";
import { useReactFlow } from "reactflow";
import {
HistoryItem,
UseUndoRedoOptions,
undoRedoContextType,
} from "../types/typesContext";
import { isWrappedWithClass } from "../utils/utils";
import { TabsContext } from "./tabsContext";
type undoRedoContextType = {
undo: () => void;
redo: () => void;
takeSnapshot: () => void;
};
type UseUndoRedoOptions = {
maxHistorySize: number;
enableShortcuts: boolean;
};
type HistoryItem = {
nodes: Node[];
edges: Edge[];
};
const initialValue = {
undo: () => {},
redo: () => {},

View file

@ -1,6 +1,10 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect, useRef } from "react";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { useNavigate } from "react-router-dom";
import { renewAccessToken } from ".";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
@ -8,52 +12,99 @@ const api: AxiosInstance = axios.create({
});
function ApiInterceptor() {
const retryCounts = useRef([]);
const { setErrorData } = useContext(alertContext);
let { accessToken, login, logout, authenticationErrorCount } =
useContext(AuthContext);
const navigate = useNavigate();
const cookies = new Cookies();
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
// return Promise.reject(error);
// }
// let retryCount = 0;
// while (retryCount < 4) {
// await sleep(5000); // Sleep for 5 seconds
// retryCount++;
// try {
// const response = await axios.request(error.config);
// return response;
// } catch (error) {
// if (retryCount === 3) {
// setErrorData({
// title: "There was an error on web connection, please: ",
// list: [
// "Refresh the page",
// "Use a new flow tab",
// "Check if the backend is up",
// "Endpoint: " + error.config?.url,
// ],
// });
// return Promise.reject(error);
// }
// }
// }
if (error.response?.status === 401) {
const refreshToken = cookies.get("refresh_token");
if (refreshToken) {
authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) {
authenticationErrorCount = 0;
logout();
navigate("/login");
}
const res = await renewAccessToken(refreshToken);
login(res.data.access_token, res.data.refresh_token);
try {
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${accessToken}`;
const response = await axios.request(error.config);
return response;
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
logout();
navigate("/login");
}
}
}
if (!refreshToken && error?.config?.url?.includes("login")) {
return Promise.reject(error);
} else {
logout();
navigate("/login");
}
} else {
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
return Promise.reject(error);
// }
}
}
);
// Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
if (
config?.url?.includes(
"https://raw.githubusercontent.com/logspace-ai/langflow_examples/main/examples"
) ||
config?.url?.includes(
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples"
) ||
config?.url?.includes(
"https://api.github.com/repos/logspace-ai/langflow"
) ||
config?.url?.includes("auto_login")
) {
delete config.headers["Authorization"];
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
return () => {
// Clean up the interceptor when the component unmounts
// Clean up the interceptors when the component unmounts
api.interceptors.response.eject(interceptor);
api.interceptors.request.eject(requestInterceptor);
};
}, [retryCounts]);
}, [accessToken, setErrorData]);
return null;
}
// Function to sleep for a given duration in milliseconds
function sleep(ms) {
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View file

@ -1,7 +1,14 @@
import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow";
import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import { APIObjectType, sendAllProps } from "../../types/api/index";
import {
APIObjectType,
LoginType,
Users,
sendAllProps,
} from "../../types/api/index";
import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import {
APIClassType,
@ -23,7 +30,7 @@ export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
const GITHUB_API_URL = "https://api.github.com";
export async function getRepoStars(owner, repo) {
export async function getRepoStars(owner: string, repo: string) {
try {
const response = await api.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`);
return response.data.stargazers_count;
@ -100,7 +107,7 @@ export async function getExamples(): Promise<FlowType[]> {
export async function saveFlowToDatabase(newFlow: {
name: string;
id: string;
data: ReactFlowJsonObject;
data: ReactFlowJsonObject | null;
description: string;
style?: FlowStyleType;
}): Promise<FlowType> {
@ -179,7 +186,7 @@ export async function downloadFlowsFromDatabase() {
}
}
export async function uploadFlowsToDatabase(flows) {
export async function uploadFlowsToDatabase(flows: FormData) {
try {
const response = await api.post(`/api/v1/flows/upload/`, flows);
@ -346,3 +353,153 @@ export async function postCustomComponent(
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`/api/v1/custom_component`, { code });
}
export async function onLogin(user: LoginType) {
try {
const response = await api.post(
`${BASE_URL_API}login`,
new URLSearchParams({
username: user.username,
password: user.password,
}).toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (response.status === 200) {
const data = response.data;
return data;
}
} catch (error) {
throw error;
}
}
export async function autoLogin() {
try {
const response = await api.get(`${BASE_URL_API}auto_login`);
if (response.status === 200) {
const data = response.data;
return data;
}
} catch (error) {
throw error;
}
}
export async function renewAccessToken(token: string) {
try {
return await api.post(`${BASE_URL_API}refresh?token=${token}`);
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getLoggedUser(): Promise<Users> {
try {
const res = await api.get(`${BASE_URL_API}user`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function addUser(user: UserInputType): Promise<Users> {
try {
const res = await api.post(`${BASE_URL_API}user`, user);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getUsersPage(
skip: number,
limit: number
): Promise<[Users]> {
try {
const res = await api.get(
`${BASE_URL_API}users?skip=${skip}&limit=${limit}`
);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function deleteUser(user_id: string) {
try {
const res = await api.delete(`${BASE_URL_API}user/${user_id}`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function updateUser(user_id: string, user: Users) {
try {
const res = await api.patch(`${BASE_URL_API}user/${user_id}`, user);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getApiKey(user_id: String) {
try {
const res = await api.get(`${BASE_URL_API}api_key/${user_id}`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function createApiKey(user_id: string) {
try {
const res = await api.post(`${BASE_URL_API}api_key/${user_id}`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function deleteApiKey(user_id: string) {
try {
const res = await api.delete(`${BASE_URL_API}api_key/${user_id}`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}

View file

@ -1,22 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 841.89 595.28" style="enable-background:new 0 0 841.89 595.28;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#505AA5;}
</style>
<path class="st0" d="M349.6,124.45c48.94-54.99,129.98-71.12,196.61-39.38c88.52,42.17,120.82,149.6,72.62,232.48L510.41,503.76
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.26-225.49c34.97-60.15,11.58-138.11-52.6-168.8
c-48.16-23.03-107.02-11.53-142.6,28.08c-19.62,21.74-30.64,49.82-31.01,79.01c-0.37,29.2,9.94,57.53,29.01,79.76
c3.43,3.99,7.12,7.75,11.04,11.25l-76.63,131.88c-3,5.16-6.99,9.67-11.74,13.3c-4.76,3.62-10.18,6.28-15.97,7.82
c-5.79,1.54-11.83,1.93-17.77,1.16c-5.94-0.78-11.67-2.71-16.87-5.68l83.19-143.17c-11.95-17.11-20.53-36.31-25.29-56.58
L261.1,360.8c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.83-226.76
C336.06,141.32,342.43,132.55,349.6,124.45z M501.76,196.63c31.75,18.21,42.71,58.7,24.34,90.22L399.69,503.74
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l117.38-201.93c-9.42-1.97-18.29-5.94-26.01-11.65
c-7.72-5.71-14.1-13.01-18.7-21.4c-4.6-8.4-7.31-17.68-7.95-27.22c-0.64-9.54,0.82-19.1,4.27-28.02c3.45-8.92,8.8-17,15.7-23.67
c6.9-6.67,15.17-11.77,24.24-14.95c9.08-3.18,18.74-4.37,28.32-3.49S493.43,191.83,501.76,196.63z M455.78,237.39
c-2.17,1.66-4,3.72-5.36,6.08h-0.01c-2.06,3.55-3.02,7.62-2.75,11.71c0.27,4.09,1.76,8,4.27,11.25c2.51,3.25,5.94,5.69,9.84,7
c3.91,1.32,8.12,1.45,12.1,0.39c3.99-1.06,7.56-3.27,10.28-6.35c2.72-3.08,4.46-6.89,5-10.95c0.54-4.06-0.15-8.19-1.97-11.87
c-1.82-3.67-4.71-6.73-8.28-8.78c-2.37-1.36-4.99-2.24-7.71-2.6c-2.72-0.35-5.48-0.18-8.12,0.53
C460.43,234.52,457.95,235.73,455.78,237.39z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 841.89 595.28" style="enable-background:new 0 0 841.89 595.28;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#505AA5;}
</style>
<path class="st0" d="M349.6,124.45c48.94-54.99,129.98-71.12,196.61-39.38c88.52,42.17,120.82,149.6,72.62,232.48L510.41,503.76
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.26-225.49c34.97-60.15,11.58-138.11-52.6-168.8
c-48.16-23.03-107.02-11.53-142.6,28.08c-19.62,21.74-30.64,49.82-31.01,79.01c-0.37,29.2,9.94,57.53,29.01,79.76
c3.43,3.99,7.12,7.75,11.04,11.25l-76.63,131.88c-3,5.16-6.99,9.67-11.74,13.3c-4.76,3.62-10.18,6.28-15.97,7.82
c-5.79,1.54-11.83,1.93-17.77,1.16c-5.94-0.78-11.67-2.71-16.87-5.68l83.19-143.17c-11.95-17.11-20.53-36.31-25.29-56.58
L261.1,360.8c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.83-226.76
C336.06,141.32,342.43,132.55,349.6,124.45z M501.76,196.63c31.75,18.21,42.71,58.7,24.34,90.22L399.69,503.74
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l117.38-201.93c-9.42-1.97-18.29-5.94-26.01-11.65
c-7.72-5.71-14.1-13.01-18.7-21.4c-4.6-8.4-7.31-17.68-7.95-27.22c-0.64-9.54,0.82-19.1,4.27-28.02c3.45-8.92,8.8-17,15.7-23.67
c6.9-6.67,15.17-11.77,24.24-14.95c9.08-3.18,18.74-4.37,28.32-3.49S493.43,191.83,501.76,196.63z M455.78,237.39
c-2.17,1.66-4,3.72-5.36,6.08h-0.01c-2.06,3.55-3.02,7.62-2.75,11.71c0.27,4.09,1.76,8,4.27,11.25c2.51,3.25,5.94,5.69,9.84,7
c3.91,1.32,8.12,1.45,12.1,0.39c3.99-1.06,7.56-3.27,10.28-6.35c2.72-3.08,4.46-6.89,5-10.95c0.54-4.06-0.15-8.19-1.97-11.87
c-1.82-3.67-4.71-6.73-8.28-8.78c-2.37-1.36-4.99-2.24-7.71-2.6c-2.72-0.35-5.48-0.18-8.12,0.53
C460.43,234.52,457.95,235.73,455.78,237.39z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

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