merge dev

This commit is contained in:
Cristhian Zanforlin Lousa 2023-09-17 10:32:45 -03:00
commit 0838baa50a
76 changed files with 2486 additions and 1555 deletions

View file

@ -38,6 +38,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
generateReleaseNotes: true
prerelease: true
tag: v${{ steps.check-version.outputs.version }}
commit: main
- name: Publish to PyPI

View file

@ -41,10 +41,10 @@ run_frontend:
cd src/frontend && npm start
run_cli:
poetry run langflow --path src/frontend/build
poetry run langflow run --path src/frontend/build
run_cli_debug:
poetry run langflow --path src/frontend/build --log-level debug
poetry run langflow run --path src/frontend/build --log-level debug
setup_devcontainer:
make init
@ -69,7 +69,7 @@ backend:
build_and_run:
echo 'Removing dist folder'
rm -rf dist
make build && poetry run pip install dist/*.tar.gz && poetry run langflow
make build && poetry run pip install dist/*.tar.gz && poetry run langflow run
build_and_install:
echo 'Removing dist folder'

View file

@ -245,7 +245,7 @@ print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))
## Deploy on Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/Emy2sU?referralCode=MnPSdg)
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg)
## Deploy on Render

View file

@ -1,11 +1,13 @@
import Admonition from '@theme/Admonition';
import Admonition from "@theme/Admonition";
# Text Splitters
<Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION">
<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>
<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>
A text splitter is a tool that divides a document or text into smaller chunks or segments. It is used to break down large texts into more manageable pieces for analysis or processing.
@ -22,13 +24,13 @@ The `CharacterTextSplitter` is used to split a long text into smaller chunks bas
- **chunk_overlap:** Determines the number of characters that overlap between consecutive chunks when splitting text. It specifies how much of the previous chunk should be included in the next chunk.
For example, if the `chunk_overlap` is set to 20 and the `chunk_size` is set to 100, the splitter will create chunks of 100 characters each, but the last 20 characters of each chunk will overlap with the first 20 characters of the next chunk. This allows for a smoother transition between chunks and ensures that no information is lost defaults to `200`.
For example, if the `chunk_overlap` is set to 20 and the `chunk_size` is set to 100, the splitter will create chunks of 100 characters each, but the last 20 characters of each chunk will overlap with the first 20 characters of the next chunk. This allows for a smoother transition between chunks and ensures that no information is lost defaults to `200`.
- **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk.
For example, if the chunk_size is set to 100, the splitter will create chunks of 100 characters each. If the text is longer than 100 characters, it will be divided into multiple chunks of equal size, except for the last chunk, which may be smaller if there are remaining characters defaults to `1000`.
For example, if the chunk_size is set to 100, the splitter will create chunks of 100 characters each. If the text is longer than 100 characters, it will be divided into multiple chunks of equal size, except for the last chunk, which may be smaller if there are remaining characters defaults to `1000`.
- **separator:** Specifies the character that will be used to split the text into chunks defaults to `.`
- **separator:** Specifies the character that will be used to split the text into chunks defaults to `.`
---
@ -44,6 +46,18 @@ The `RecursiveCharacterTextSplitter` splits the text by trying to keep paragra
- **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk.
- **separator_type:** The parameter allows the user to split the code with multiple language support. It supports various languages such as Text, Ruby, Python, Solidity, Java, and more. Defaults to `Text`.
- **separators:** The `separators` in RecursiveCharacterTextSplitter are the characters used to split the text into chunks. The text splitter tries to create chunks based on splitting on the first character in the list of `separators`. If any chunks are too large, it moves on to the next character in the list and continues splitting. Defaults to ["\n\n", "\n", " ", ""].
- **separators:** The `separators` in RecursiveCharacterTextSplitter are the characters used to split the text into chunks. The text splitter tries to create chunks based on splitting on the first character in the list of `separators`. If any chunks are too large, it moves on to the next character in the list and continues splitting. Defaults to `.`
### LanguageRecursiveTextSplitter
The `LanguageRecursiveTextSplitter` is a text splitter that splits the text into smaller chunks based on the (programming) language of the text.
**Params**
- **Documents:** Input documents to split.
- **chunk_overlap:** Determines the number of characters that overlap between consecutive chunks when splitting text. It specifies how much of the previous chunk should be included in the next chunk.
- **chunk_size:** Determines the maximum number of characters in each chunk when splitting a text. It specifies the size or length of each chunk.
- **separator_type:** The parameter allows the user to split the code with multiple language support. It supports various languages such as Ruby, Python, Solidity, Java, and more. Defaults to `Python`.

View file

@ -0,0 +1,49 @@
# Integrating Langfuse with Langflow
## Introduction
Langfuse is an open-source tracing and analytics tool designed for LLM applications. Integrating Langfuse with Langflow provides detailed production traces and granular insights into quality, cost, and latency. This integration allows you to monitor and debug your Langflow's chat or APIs easily.
## Step-by-Step Instructions
### Step 1: Create a Langfuse account
1. Go to [Langfuse](https://langfuse.com) and click on the "Sign In" button in the top right corner.
2. Click on the "Sign Up" button and create an account.
3. Once logged in, click on "Settings" and then on "Create new API keys."
4. Copy the Public key and the Secret Key and save them somewhere safe.
{/* Add these keys to your environment variables in the following step. */}
### Step 2: Set up Langfuse in Langflow
1. **Export the Environment Variables**: You'll need to export the environment variables `LANGFLOW_LANGFUSE_SECRET_KEY` and `LANGFLOW_LANGFUSE_PUBLIC_KEY` with the values obtained in Step 1.
You can do this by executing the following commands in your terminal:
```bash
export LANGFLOW_LANGFUSE_SECRET_KEY=<your secret key>
export LANGFLOW_LANGFUSE_PUBLIC_KEY=<your public key>
```
Alternatively, you can run the Langflow CLI command:
```bash
LANGFLOW_LANGFUSE_SECRET_KEY=<your secret key> LANGFLOW_LANGFUSE_PUBLIC_KEY=<your public key> langflow
```
If you are self-hosting Langfuse, you can also set the environment variable `LANGFLOW_LANGFUSE_HOST` to point to your Langfuse instance. By default, Langfuse points to the cloud instance at `https://cloud.langfuse.com`.
2. **Verify Integration**: Ensure that the environment variables are set correctly by checking their existence in your environment, for example by running:
```bash
echo $LANGFLOW_LANGFUSE_SECRET_KEY
echo $LANGFLOW_LANGFUSE_PUBLIC_KEY
```
3. **Monitor Langflow**: Now, whenever you use Langflow's chat or API, you will be able to see the tracing of your conversations in Langfuse.
That's it! You have successfully integrated Langfuse with Langflow, enhancing observability and debugging capabilities for your LLM application.
---
Note: For more details or customized configurations, please refer to the official [Langfuse documentation](https://langfuse.com/docs/integrations/langchain).

View file

@ -51,7 +51,11 @@ module.exports = {
type: "category",
label: "Step-by-Step Guides",
collapsed: false,
items: ["guides/loading_document", "guides/chatprompttemplate_guide"],
items: [
"guides/loading_document",
"guides/chatprompttemplate_guide",
"guides/langfuse_integration",
],
},
// {
// type: 'category',

1444
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -32,17 +32,17 @@ beautifulsoup4 = "^4.12.2"
google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0"
typer = "^0.9.0"
gunicorn = "^21.1.0"
gunicorn = "^21.2.0"
langchain = "^0.0.274"
openai = "^0.27.8"
pandas = "^2.0.0"
chromadb = "^0.3.21"
chromadb = "^0.3.0"
huggingface-hub = { version = "^0.16.0", extras = ["inference"] }
rich = "^13.4.2"
rich = "^13.5.0"
llama-cpp-python = { version = "~0.1.0", optional = true }
networkx = "^3.1"
unstructured = "^0.7.0"
pypdf = "^3.11.0"
unstructured = "^0.10.0"
pypdf = "^3.15.0"
lxml = "^4.9.2"
pysrt = "^1.1.2"
fake-useragent = "^1.2.1"
@ -52,13 +52,13 @@ pyarrow = "^12.0.0"
tiktoken = "~0.4.0"
wikipedia = "^1.4.0"
langchain-serve = { version = ">0.0.51", optional = true }
qdrant-client = "^1.3.0"
qdrant-client = "^1.4.0"
websockets = "^10.3"
weaviate-client = "^3.21.0"
weaviate-client = "^3.23.0"
jina = "3.15.2"
sentence-transformers = { version = "^2.2.2", optional = true }
ctransformers = { version = "^0.2.10", optional = true }
cohere = "^4.11.0"
cohere = "^4.21.0"
python-multipart = "^0.0.6"
sqlmodel = "^0.0.8"
faiss-cpu = "^1.7.4"
@ -82,9 +82,12 @@ passlib = "^1.7.4"
bcrypt = "^4.0.1"
python-jose = "^3.3.0"
metaphor-python = "^0.1.11"
markupsafe = "^2.1.3"
pywin32 = { version = "^306", markers = "sys_platform == 'win32'" }
loguru = "^0.7.1"
langfuse = "^1.0.13"
pillow = "^10.0.0"
metal-sdk = "^2.0.2"
markupsafe = "^2.1.3"
[tool.poetry.group.dev.dependencies]
black = "^23.1.0"
@ -105,6 +108,7 @@ types-passlib = "^1.7.7.13"
pytest-mock = "^3.11.1"
pytest-xdist = "^3.3.1"
types-pywin32 = "^306.0.0.4"
types-google-cloud-ndb = "^2.2.0.0"
[tool.poetry.extras]

View file

@ -3,9 +3,14 @@ services:
- type: web
name: langflow
runtime: docker
plan: free
dockerfilePath: ./Dockerfile
repo: https://github.com/logspace-ai/langflow
branch: main
healthCheckPath: /health
autoDeploy: false
envVars:
- key: LANGFLOW_DATABASE_URL
value: sqlite:////home/user/.cache/langflow/langflow.db
disk:
name: langflow-data
mountPath: /home/user/.cache/langflow

View file

@ -0,0 +1,49 @@
"""Add profile-image column
Revision ID: 67cc006d50bf
Revises: 260dbcc8b680
Create Date: 2023-09-08 07:36:13.387318
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision: str = "67cc006d50bf"
down_revision: Union[str, None] = "260dbcc8b680"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
if "user" in inspector.get_table_names() and "profile_image" not in [
column["name"] for column in inspector.get_columns("user")
]:
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"profile_image", sqlmodel.sql.sqltypes.AutoString(), nullable=True
)
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
if "user" in inspector.get_table_names() and "profile_image" in [
column["name"] for column in inspector.get_columns("user")
]:
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("profile_image")
# ### end Alembic commands ###

View file

@ -13,7 +13,6 @@ from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, St
from langflow.graph.graph.base import Graph
from langflow.services.auth.utils import get_current_active_user, get_current_user
from langflow.services.utils import get_session
from loguru import logger
from langflow.services.utils import get_chat_manager, get_session
from cachetools import LRUCache

View file

@ -18,15 +18,17 @@ from langflow.services.auth.utils import (
get_current_active_superuser,
get_current_active_user,
get_password_hash,
verify_password,
)
from langflow.services.database.models.user.crud import (
get_user_by_id,
update_user,
)
router = APIRouter(tags=["Users"])
router = APIRouter(tags=["Users"], prefix="/users")
@router.post("/user", response_model=UserRead, status_code=201)
@router.post("/", response_model=UserRead, status_code=201)
def add_user(
user: UserCreate,
session: Session = Depends(get_session),
@ -50,7 +52,7 @@ def add_user(
return new_user
@router.get("/user", response_model=UserRead)
@router.get("/whoami", response_model=UserRead)
def read_current_user(
current_user: User = Depends(get_current_active_user),
) -> User:
@ -60,7 +62,7 @@ def read_current_user(
return current_user
@router.get("/users", response_model=UsersResponse)
@router.get("/", response_model=UsersResponse)
def read_all_users(
skip: int = 0,
limit: int = 10,
@ -82,20 +84,61 @@ def read_all_users(
)
@router.patch("/user/{user_id}", response_model=UserRead)
@router.patch("/{user_id}", response_model=UserRead)
def patch_user(
user_id: UUID,
user: UserUpdate,
_: Session = Depends(get_current_active_user),
user_update: UserUpdate,
user: User = Depends(get_current_active_user),
session: Session = Depends(get_session),
) -> User:
"""
Update an existing user's data.
"""
return update_user(user_id, user, session)
if not user.is_superuser and user.id != user_id:
raise HTTPException(
status_code=403, detail="You don't have the permission to update this user"
)
if user_update.password:
raise HTTPException(
status_code=400, detail="You can't change your password here"
)
if user_db := get_user_by_id(session, user_id):
return update_user(user_db, user_update, session)
else:
raise HTTPException(status_code=404, detail="User not found")
@router.delete("/user/{user_id}")
@router.patch("/{user_id}/reset-password", response_model=UserRead)
def reset_password(
user_id: UUID,
user_update: UserUpdate,
user: User = Depends(get_current_active_user),
session: Session = Depends(get_session),
) -> User:
"""
Reset a user's password.
"""
if user_id != user.id:
raise HTTPException(
status_code=400, detail="You can't change another user's password"
)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if verify_password(user_update.password, user.password):
raise HTTPException(
status_code=400, detail="You can't use your current password"
)
new_password = get_password_hash(user_update.password)
user.password = new_password
session.commit()
session.refresh(user)
return user
@router.delete("/{user_id}", response_model=dict)
def delete_user(
user_id: UUID,
current_user: User = Depends(get_current_active_superuser),

View file

@ -42,8 +42,8 @@ class ConversationalAgent(CustomComponent):
self,
model_name: str,
openai_api_key: str,
openai_api_base: str,
tools: Tool,
openai_api_base: Optional[str] = None,
memory: Optional[BaseMemory] = None,
system_message: Optional[SystemMessagePromptTemplate] = None,
max_token_limit: int = 2000,

View file

@ -0,0 +1,42 @@
from typing import Optional
from langflow import CustomComponent
from langchain.llms import HuggingFaceEndpoint
from langchain.llms.base import BaseLLM
class HuggingFaceEndpointsComponent(CustomComponent):
display_name: str = "Hugging Face Inference API"
description: str = "LLM model from Hugging Face Inference API."
def build_config(self):
return {
"endpoint_url": {"display_name": "Endpoint URL", "password": True},
"task": {
"display_name": "Task",
"type": "select",
"options": ["text2text-generation", "text-generation", "summarization"],
},
"huggingfacehub_api_token": {"display_name": "API token", "password": True},
"model_kwargs": {
"display_name": "Model Keyword Arguments",
"field_type": "code",
},
"code": {"show": False},
}
def build(
self,
endpoint_url: str,
task="text2text-generation",
huggingfacehub_api_token: Optional[str] = None,
model_kwargs: Optional[dict] = None,
) -> BaseLLM:
try:
output = HuggingFaceEndpoint(
endpoint_url=endpoint_url,
task=task,
huggingfacehub_api_token=huggingfacehub_api_token,
)
except Exception as e:
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
return output

View file

@ -0,0 +1,28 @@
from typing import Optional
from langflow import CustomComponent
from langchain.retrievers import MetalRetriever
from langchain.schema import BaseRetriever
from metal_sdk.metal import Metal # type: ignore
class MetalRetrieverComponent(CustomComponent):
display_name: str = "Metal Retriever"
description: str = "Retriever that uses the Metal API."
def build_config(self):
return {
"api_key": {"display_name": "API Key", "password": True},
"client_id": {"display_name": "Client ID", "password": True},
"index_id": {"display_name": "Index ID"},
"params": {"display_name": "Parameters", "field_type": "code"},
"code": {"show": False},
}
def build(
self, api_key: str, client_id: str, index_id: str, params: Optional[dict] = None
) -> BaseRetriever:
try:
metal = Metal(api_key=api_key, client_id=client_id, index_id=index_id)
except Exception as e:
raise ValueError("Could not connect to Metal API.") from e
return MetalRetriever(client=metal, params=params or {})

View file

@ -0,0 +1,80 @@
from typing import Optional
from langflow import CustomComponent
from langchain.text_splitter import Language
from langchain.schema import Document
class LanguageRecursiveTextSplitterComponent(CustomComponent):
display_name: str = "Language Recursive Text Splitter"
description: str = "Split text into chunks of a specified length based on language."
documentation: str = "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter"
def build_config(self):
options = [x.value for x in Language]
return {
"documents": {
"display_name": "Documents",
"info": "The documents to split.",
},
"separator_type": {
"display_name": "Separator Type",
"info": "The type of separator to use.",
"field_type": "str",
"options": options,
"value": "Python",
},
"separators": {
"display_name": "Separators",
"info": "The characters to split on.",
"is_list": True,
},
"chunk_size": {
"display_name": "Chunk Size",
"info": "The maximum length of each chunk.",
"field_type": "int",
"value": 1000,
},
"chunk_overlap": {
"display_name": "Chunk Overlap",
"info": "The amount of overlap between chunks.",
"field_type": "int",
"value": 200,
},
"code": {"show": False},
}
def build(
self,
documents: list[Document],
chunk_size: Optional[int] = 1000,
chunk_overlap: Optional[int] = 200,
separator_type: Optional[str] = "Python",
) -> list[Document]:
"""
Split text into chunks of a specified length.
Args:
separators (list[str]): The characters to split on.
chunk_size (int): The maximum length of each chunk.
chunk_overlap (int): The amount of overlap between chunks.
length_function (function): The function to use to calculate the length of the text.
Returns:
list[str]: The chunks of text.
"""
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Make sure chunk_size and chunk_overlap are ints
if isinstance(chunk_size, str):
chunk_size = int(chunk_size)
if isinstance(chunk_overlap, str):
chunk_overlap = int(chunk_overlap)
splitter = RecursiveCharacterTextSplitter.from_language(
language=Language(separator_type),
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
docs = splitter.split_documents(documents)
return docs

View file

@ -0,0 +1,79 @@
from typing import Optional
from langflow import CustomComponent
from langchain.schema import Document
from langflow.utils.util import build_loader_repr_from_documents
class RecursiveCharacterTextSplitterComponent(CustomComponent):
display_name: str = "Recursive Character Text Splitter"
description: str = "Split text into chunks of a specified length."
documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter"
def build_config(self):
return {
"documents": {
"display_name": "Documents",
"info": "The documents to split.",
},
"separators": {
"display_name": "Separators",
"info": 'The characters to split on.\nIf left empty defaults to ["\\n\\n", "\\n", " ", ""].',
"is_list": True,
},
"chunk_size": {
"display_name": "Chunk Size",
"info": "The maximum length of each chunk.",
"field_type": "int",
"value": 1000,
},
"chunk_overlap": {
"display_name": "Chunk Overlap",
"info": "The amount of overlap between chunks.",
"field_type": "int",
"value": 200,
},
"code": {"show": False},
}
def build(
self,
documents: list[Document],
separators: Optional[list[str]] = None,
chunk_size: Optional[int] = 1000,
chunk_overlap: Optional[int] = 200,
) -> list[Document]:
"""
Split text into chunks of a specified length.
Args:
separators (list[str]): The characters to split on.
chunk_size (int): The maximum length of each chunk.
chunk_overlap (int): The amount of overlap between chunks.
length_function (function): The function to use to calculate the length of the text.
Returns:
list[str]: The chunks of text.
"""
from langchain.text_splitter import RecursiveCharacterTextSplitter
if separators == "":
separators = None
elif separators:
# check if the separators list has escaped characters
# if there are escaped characters, unescape them
separators = [x.encode().decode("unicode-escape") for x in separators]
# Make sure chunk_size and chunk_overlap are ints
if isinstance(chunk_size, str):
chunk_size = int(chunk_size)
if isinstance(chunk_overlap, str):
chunk_overlap = int(chunk_overlap)
splitter = RecursiveCharacterTextSplitter(
separators=separators,
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
docs = splitter.split_documents(documents)
self.repr_value = build_loader_repr_from_documents(docs)
return docs

View file

@ -171,8 +171,6 @@ prompts:
textsplitters:
CharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"
RecursiveCharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/recursive_text_splitter"
toolkits:
OpenAPIToolkit:
documentation: ""

View file

@ -5,6 +5,7 @@ from langflow.api.utils import merge_nested_dicts_with_renaming
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.utils import extract_inner_type
from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator
from langflow.interface.importing.utils import get_function_custom
@ -84,6 +85,8 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
def process_type(field_type: str):
if field_type.startswith("list") or field_type.startswith("List"):
return extract_inner_type(field_type)
return "prompt" if field_type == "Prompt" else field_type
@ -100,6 +103,7 @@ def add_new_custom_field(
# if it is, update the value
display_name = field_config.pop("display_name", field_name)
field_type = field_config.pop("field_type", field_type)
field_contains_list = "list" in field_type.lower()
field_type = process_type(field_type)
field_value = field_config.pop("value", field_value)
field_advanced = field_config.pop("advanced", False)
@ -110,7 +114,9 @@ def add_new_custom_field(
# If options is a list, then it's a dropdown
# If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list)
field_config["is_list"] = is_list or field_config.get("is_list", False)
field_config["is_list"] = (
is_list or field_config.get("is_list", False) or field_contains_list
)
if "name" in field_config:
warnings.warn(
@ -172,7 +178,7 @@ def extract_type_from_optional(field_type):
Returns:
str: The extracted type, or an empty string if no type was found.
"""
match = re.search(r"\[(.*?)\]", field_type)
match = re.search(r"\[(.*?)\]$", field_type)
return match[1] if match else None

View file

@ -11,6 +11,7 @@ from langflow.api import router
from langflow.interface.utils import setup_llm_caching
from langflow.services.database.utils import initialize_database
from langflow.services.manager import initialize_services, teardown_services
from langflow.services.plugins.langfuse import LangfuseInstance
from langflow.utils.logger import configure
@ -41,6 +42,8 @@ def create_app():
app.on_event("startup")(initialize_database)
app.on_event("startup")(setup_llm_caching)
app.on_event("shutdown")(teardown_services)
app.on_event("startup")(LangfuseInstance.update)
app.on_event("shutdown")(LangfuseInstance.teardown)
return app

View file

@ -1,4 +1,4 @@
from typing import Union
from typing import List, Union, TYPE_CHECKING
from langflow.api.v1.callback import (
AsyncStreamingLLMCallbackHandler,
StreamingLLMCallbackHandler,
@ -6,6 +6,52 @@ from langflow.api.v1.callback import (
from langflow.processing.process import fix_memory_inputs, format_actions
from loguru import logger
from langchain.agents.agent import AgentExecutor
from langchain.callbacks.base import BaseCallbackHandler
if TYPE_CHECKING:
from langfuse.callback import CallbackHandler # type: ignore
def setup_callbacks(sync, trace_id, **kwargs):
"""Setup callbacks for langchain object"""
callbacks = []
if sync:
callbacks.append(StreamingLLMCallbackHandler(**kwargs))
else:
callbacks.append(AsyncStreamingLLMCallbackHandler(**kwargs))
if langfuse_callback := get_langfuse_callback(trace_id=trace_id):
logger.debug("Langfuse callback loaded")
callbacks.append(langfuse_callback)
return callbacks
def get_langfuse_callback(trace_id):
from langflow.services.plugins.langfuse import LangfuseInstance
from langfuse.callback import CreateTrace
logger.debug("Initializing langfuse callback")
if langfuse := LangfuseInstance.get():
logger.debug("Langfuse credentials found")
try:
trace = langfuse.trace(CreateTrace(id=trace_id))
return trace.getNewHandler()
except Exception as exc:
logger.error(f"Error initializing langfuse callback: {exc}")
return None
def flush_langfuse_callback_if_present(
callbacks: List[Union[BaseCallbackHandler, "CallbackHandler"]]
):
"""
If langfuse callback is present, run callback.langfuse.flush()
"""
for callback in callbacks:
if hasattr(callback, "langfuse"):
callback.langfuse.flush()
break
async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwargs):
@ -27,13 +73,18 @@ async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwa
logger.error(f"Error fixing memory inputs: {exc}")
try:
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
output = await langchain_object.acall(inputs, callbacks=async_callbacks)
trace_id = kwargs.pop("session_id", None)
callbacks = setup_callbacks(sync=False, trace_id=trace_id, **kwargs)
output = await langchain_object.acall(inputs, callbacks=callbacks)
except Exception as exc:
# make the error message more informative
logger.debug(f"Error: {str(exc)}")
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
output = langchain_object(inputs, callbacks=sync_callbacks)
trace_id = kwargs.pop("session_id", None)
callbacks = setup_callbacks(sync=True, trace_id=trace_id, **kwargs)
output = langchain_object(inputs, callbacks=callbacks)
# if langfuse callback is present, run callback.langfuse.flush()
flush_langfuse_callback_if_present(callbacks)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []

View file

@ -11,6 +11,7 @@ from langflow.graph import Graph
from langchain.chains.base import Chain
from langchain.vectorstores.base import VectorStore
from typing import Any, Dict, List, Optional, Tuple, Union
from langchain.schema import Document
def fix_memory_inputs(langchain_object):
@ -142,6 +143,8 @@ def generate_result(langchain_object: Union[Chain, VectorStore], inputs: dict):
logger.debug("Generated result and thought")
elif isinstance(langchain_object, VectorStore):
result = langchain_object.search(**inputs)
elif isinstance(langchain_object, Document):
result = langchain_object.dict()
else:
raise ValueError(
f"Unknown langchain_object type: {type(langchain_object).__name__}"

View file

@ -1,4 +1,5 @@
from collections import defaultdict
import uuid
from fastapi import WebSocket, status
from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse
from langflow.services.base import Service
@ -49,6 +50,7 @@ class ChatManager(Service):
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {}
self.connection_ids: Dict[str, str] = {}
self.chat_history = ChatHistory()
self.cache_manager = service_manager.get(ServiceType.CACHE_MANAGER)
self.cache_manager.attach(self.update)
@ -93,9 +95,13 @@ class ChatManager(Service):
async def connect(self, client_id: str, websocket: WebSocket):
self.active_connections[client_id] = websocket
# This is to avoid having multiple clients with the same id
#! Temporary solution
self.connection_ids[client_id] = f"{client_id}-{uuid.uuid4()}"
def disconnect(self, client_id: str):
self.active_connections.pop(client_id, None)
self.connection_ids.pop(client_id, None)
async def send_message(self, client_id: str, message: str):
websocket = self.active_connections[client_id]
@ -137,6 +143,7 @@ class ChatManager(Service):
langchain_object=langchain_object,
chat_inputs=chat_inputs,
websocket=self.active_connections[client_id],
session_id=self.connection_ids[client_id],
)
except Exception as e:
# Log stack trace

View file

@ -9,6 +9,7 @@ async def process_graph(
langchain_object,
chat_inputs: ChatMessage,
websocket: WebSocket,
session_id: str,
):
langchain_object = try_setting_streaming_options(langchain_object, websocket)
logger.debug("Loaded langchain object")
@ -27,7 +28,10 @@ async def process_graph(
logger.debug("Generating result and thought")
result, intermediate_steps = await get_result_and_steps(
langchain_object, chat_inputs.message, websocket=websocket
langchain_object,
chat_inputs.message,
websocket=websocket,
session_id=session_id,
)
logger.debug("Generated result and intermediate_steps")
return result, intermediate_steps

View file

@ -1,12 +1,12 @@
from datetime import datetime, timezone
from typing import Union
from uuid import UUID
from fastapi import Depends, HTTPException
from fastapi import Depends, HTTPException, status
from langflow.services.database.models.user.user import User, UserUpdate
from langflow.services.utils import get_session
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session
from typing import Optional
from sqlalchemy.orm.attributes import flag_modified
@ -20,20 +20,26 @@ def get_user_by_id(db: Session, id: UUID) -> Union[User, None]:
def update_user(
user_id: UUID, user: UserUpdate, db: Session = Depends(get_session)
user_db: Optional[User], user: UserUpdate, db: Session = Depends(get_session)
) -> User:
user_db = get_user_by_id(db, user_id)
if not user_db:
raise HTTPException(status_code=404, detail="User not found")
user_db_by_username = get_user_by_username(db, user.username) # type: ignore
if user_db_by_username and user_db_by_username.id != user_id:
raise HTTPException(status_code=409, detail="Username already exists")
# user_db_by_username = get_user_by_username(db, user.username) # type: ignore
# if user_db_by_username and user_db_by_username.id != user_id:
# raise HTTPException(status_code=409, detail="Username already exists")
user_data = user.dict(exclude_unset=True)
changed = False
for attr, value in user_data.items():
if hasattr(user_db, attr) and value is not None:
setattr(user_db, attr, value)
changed = True
if not changed:
raise HTTPException(
status_code=status.HTTP_304_NOT_MODIFIED, detail="Nothing to update"
)
user_db.updated_at = datetime.now(timezone.utc)
flag_modified(user_db, "updated_at")
@ -49,5 +55,5 @@ def update_user(
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
return update_user(user_id, user_data, db)
user = get_user_by_id(db, user_id)
return update_user(user, user_data, db)

View file

@ -15,6 +15,7 @@ class User(SQLModelSerializable, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
username: str = Field(index=True, unique=True)
password: str = Field()
profile_image: Optional[str] = Field(default=None)
is_active: bool = Field(default=False)
is_superuser: bool = Field(default=False)
create_at: datetime = Field(default_factory=datetime.utcnow)
@ -32,6 +33,7 @@ class UserCreate(SQLModel):
class UserRead(SQLModel):
id: UUID = Field(default_factory=uuid4)
username: str = Field()
profile_image: Optional[str] = Field()
is_active: bool = Field()
is_superuser: bool = Field()
create_at: datetime = Field()
@ -40,7 +42,8 @@ class UserRead(SQLModel):
class UserUpdate(SQLModel):
username: Optional[str] = Field()
profile_image: Optional[str] = Field()
password: Optional[str] = Field()
is_active: Optional[bool] = Field()
is_superuser: Optional[bool] = Field()
last_login_at: Optional[datetime] = Field()

View file

@ -0,0 +1,49 @@
from langflow.services.utils import get_settings_manager
from langflow.utils.logger import logger
### Temporary implementation
# This will be replaced by a plugin system once merged into 0.5.0
class LangfuseInstance:
_instance = None
@classmethod
def get(cls):
logger.debug("Getting Langfuse instance")
if cls._instance is None:
cls.create()
return cls._instance
@classmethod
def create(cls):
logger.debug("Creating Langfuse instance")
from langfuse import Langfuse # type: ignore
settings_manager = get_settings_manager()
if (
settings_manager.settings.LANGFUSE_PUBLIC_KEY
and settings_manager.settings.LANGFUSE_SECRET_KEY
):
logger.debug("Langfuse credentials found")
cls._instance = Langfuse(
public_key=settings_manager.settings.LANGFUSE_PUBLIC_KEY,
secret_key=settings_manager.settings.LANGFUSE_SECRET_KEY,
)
else:
logger.debug("No Langfuse credentials found")
cls._instance = None
@classmethod
def update(cls):
logger.debug("Updating Langfuse instance")
cls._instance = None
cls.create()
@classmethod
def teardown(cls):
logger.debug("Tearing down Langfuse instance")
if cls._instance is not None:
cls._instance.flush()
cls._instance = None

View file

@ -41,6 +41,10 @@ class Settings(BaseSettings):
REMOVE_API_KEYS: bool = False
COMPONENTS_PATH: List[str] = []
LANGFUSE_SECRET_KEY: Optional[str] = None
LANGFUSE_PUBLIC_KEY: Optional[str] = None
LANGFUSE_HOST: Optional[str] = None
@validator("CONFIG_DIR", pre=True, allow_reuse=True)
def set_langflow_dir(cls, value):
if not value:

View file

@ -2,12 +2,13 @@ import re
import inspect
import importlib
from functools import wraps
from typing import Optional, Dict, Any, Union
from typing import List, Optional, Dict, Any, Union
from docstring_parser import parse
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants
from langchain.schema import Document
def build_template_from_function(
@ -456,3 +457,12 @@ def add_options_to_field(
value["options"] = options_map[class_name]
value["list"] = True
value["value"] = options_map[class_name][0]
def build_loader_repr_from_documents(documents: List[Document]) -> str:
if documents:
avg_length = sum(len(doc.page_content) for doc in documents) / len(documents)
return f"""{len(documents)} documents
\nAvg. Document Length (characters): {int(avg_length)}
Documents: {documents[:3]}..."""
return "0 documents"

File diff suppressed because it is too large Load diff

View file

@ -141,45 +141,54 @@ export default function ParameterComponent({
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "flex items-center"
<>
{index === 0 && (
<span>
{left
? "Avaliable input components:"
: "Avaliable output components:"}
</span>
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
)}
>
<Icon
<div
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
color: nodeColors[item.family],
}}
/>
</div>
<span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"}
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"}{" "}
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
</span>
</span>
</span>
</>
);
});
} else {

View file

@ -158,8 +158,17 @@ export default function GenericNode({
</div>
</div>
<div className="generic-node-desc">
<div className="generic-node-desc-text">{data.node?.description}</div>
<div
className={
"generic-node-desc " +
(data.node?.description !== "" ? "py-5" : "pb-5")
}
>
{data.node?.description !== "" && (
<div className="generic-node-desc-text">
{data.node?.description}
</div>
)}
<>
{Object.keys(data.node!.template)

View file

@ -47,21 +47,9 @@ export const EditFlowSettings: React.FC<InputProps> = ({
setInvalidName!(true);
}
setName(value);
setCurrentName(value);
};
const [currentName, setCurrentName] = useState(name);
const [currentDescription, setCurrentDescription] = useState(description);
useEffect(() => {
setCurrentName(name);
setCurrentDescription(description);
}, [name, description]);
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
flows.find((f) => f.id === tabId).description = event.target.value;
setCurrentDescription(flows.find((f) => f.id === tabId).description);
setDescription(event.target.value);
};
@ -82,7 +70,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
onChange={handleNameChange}
type="text"
name="name"
value={currentName ?? ""}
value={name ?? ""}
placeholder="File name"
id="name"
maxLength={maxLength}
@ -97,7 +85,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
name="description"
id="description"
onChange={handleDescriptionChange}
value={currentDescription}
value={description}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}

View file

@ -12,17 +12,15 @@ import { Button } from "../ui/button";
export default function PaginatorComponent({
pageSize = 10,
pageIndex = 0,
pageIndex = 1,
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);
useEffect(() => {
setMaxPageIndex(Math.ceil(totalRowsCount / size));
@ -39,8 +37,9 @@ export default function PaginatorComponent({
onValueChange={(pageSize: string) => {
setPageSize(Number(pageSize));
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
paginate(Number(pageSize), 0);
paginate(Number(pageSize), 1);
}}
value={pageSize.toString()}
>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="10" />
@ -55,30 +54,25 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {currentPage} of {maxIndex}
Page {pageIndex} of {maxIndex}
</div>
<div className="flex items-center space-x-2">
<Button
disabled={index <= 0}
disabled={pageIndex <= 1}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(0);
setCurrentPage(1);
paginate(size, 0);
paginate(size, 1);
}}
>
<span className="sr-only">Go to first page</span>
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
</Button>
<Button
disabled={index <= 0}
disabled={pageIndex <= 1}
onClick={() => {
if (index > 0) {
const pgIndex = size - index;
setCurrentPage(currentPage - 1);
setPageIndex(pgIndex);
paginate(size, pgIndex);
if (pageIndex > 0) {
paginate(size, pageIndex - 1);
}
}}
variant="outline"
@ -88,12 +82,9 @@ export default function PaginatorComponent({
<IconComponent name="ChevronLeft" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
disabled={pageIndex === maxIndex}
onClick={() => {
const pgIndex = size + index;
setPageIndex(pgIndex);
setCurrentPage(currentPage + 1);
paginate(size, pgIndex);
paginate(size, pageIndex + 1);
}}
variant="outline"
className="h-8 w-8 p-0"
@ -102,13 +93,11 @@ export default function PaginatorComponent({
<IconComponent name="ChevronRight" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
disabled={pageIndex === maxIndex}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(maxIndex - 1);
setCurrentPage(maxIndex);
paginate(size, size);
paginate(size, maxIndex);
}}
>
<span className="sr-only">Go to last page</span>

View file

@ -180,7 +180,7 @@ export default function CodeTabsComponent({
key={idx} // Remember to add a unique key prop
>
{idx < 4 ? (
<div className="w-full h-full flex flex-col">
<div className="flex h-full w-full flex-col">
{tab.description && (
<div
className="mb-2 w-full text-left text-sm"

View file

@ -0,0 +1,21 @@
import { gradients } from "../../utils/styleUtils";
export default function GradientChooserComponent({ value, onChange }) {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
{gradients.map((gradient, idx) => (
<div
onClick={() => {
onChange(gradient);
}}
className={
"duration-400 h-12 w-12 cursor-pointer rounded-full transition-all " +
gradient +
(value === gradient ? " shadow-lg ring-2 ring-primary" : "")
}
key={idx}
></div>
))}
</div>
);
}

View file

@ -27,7 +27,7 @@ export default function Header(): JSX.Element {
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const { stars } = useContext(darkContext);
const { stars, gradientIndex } = useContext(darkContext);
const navigate = useNavigate();
return (
@ -139,9 +139,7 @@ export default function Header(): JSX.Element {
<button
className={
"h-7 w-7 rounded-full focus-visible:outline-0 " +
gradients[
parseInt(userData?.id ?? "", 10) % gradients.length
]
(userData?.profile_image ?? gradients[gradientIndex])
}
/>
</DropdownMenuTrigger>
@ -156,6 +154,12 @@ export default function Header(): JSX.Element {
Admin Page
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/account/settings")}
>
Profile Settings
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {

View file

@ -18,6 +18,9 @@ export default function InputListComponent({
}
}, [disabled]);
// @TODO Recursive Character Text Splitter - the value might be in string format, whereas the InputListComponent specifically requires an array format. To ensure smooth operation and prevent potential errors, it's crucial that we handle the conversion from a string to an array with the string as its element.
typeof value === "string" ? (value = [value]) : (value = value);
return (
<div
className={classNames(

View file

@ -4,10 +4,10 @@ export default function LoadingComponent({
remSize,
}: LoadingComponentProps): JSX.Element {
return (
<div role="status" className="m-auto w-min">
<div role="status" className="flex flex-col items-center justify-center">
<svg
aria-hidden="true"
className={`w-${remSize} h-${remSize} mr-2 animate-spin fill-almost-medium-blue text-muted`}
className={`w-${remSize} h-${remSize} animate-spin fill-almost-medium-blue text-muted`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View file

@ -1,7 +1,7 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
const Dialog = DialogPrimitive.Root;
@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed top-0 right-0 left-0 bottom-0 overflow-auto inset-0 z-50 bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@ -44,14 +44,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"noundo nocopy flex text-start overflow-auto z-50 justify-center items-left flex-col w-full max-w-lg gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-[48%] data-[state=open]:slide-in-from-bottom-[48%] sm:rounded-lg md:w-full",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<IconComponent name="X" className="h-4 w-4" />
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>

View file

@ -503,6 +503,21 @@ export const NOUNS: string[] = [
*/
export const USER_PROJECTS_HEADER = "My Collection";
/**
* Header text for admin page
* @constant
*
*/
export const ADMIN_HEADER_TITLE = "Admin Page";
/**
* Header description for admin page
* @constant
*
*/
export const ADMIN_HEADER_DESCRIPTION =
"Navigate through this section to efficiently oversee all application users. From here, you can seamlessly manage user accounts.";
/**
* URLs excluded from error retries.
* @constant
@ -523,6 +538,12 @@ export const CONTROL_INPUT_STATE = {
username: "",
};
export const CONTROL_PATCH_USER_STATE = {
password: "",
cnfPassword: "",
gradient: "",
};
export const CONTROL_LOGIN_STATE = {
username: "",
password: "",

View file

@ -63,7 +63,7 @@ export function AuthProvider({ children }): React.ReactElement {
.then((user) => {
setUserData(user);
setLoading(false);
const isSuperUser = user.is_superuser;
const isSuperUser = user!.is_superuser;
setIsAdmin(isSuperUser);
})
.catch((error) => {});

View file

@ -7,6 +7,8 @@ const initialValue = {
setDark: () => {},
stars: 0,
setStars: (stars) => 0,
gradientIndex: 0,
setGradientIndex: () => 0,
};
export const darkContext = createContext<darkContextType>(initialValue);
@ -16,6 +18,7 @@ export function DarkProvider({ children }) {
JSON.parse(window.localStorage.getItem("isDark")!) ?? false
);
const [stars, setStars] = useState<number>(0);
const [gradientIndex, setGradientIndex] = useState<number>(0);
useEffect(() => {
async function fetchStars() {
@ -23,6 +26,9 @@ export function DarkProvider({ children }) {
setStars(starsCount);
}
fetchStars();
const min = 0;
const max = 30;
setGradientIndex(Math.floor(Math.random() * (max - min + 1)) + min);
}, []);
useEffect(() => {
@ -41,6 +47,8 @@ export function DarkProvider({ children }) {
stars,
dark,
setDark,
setGradientIndex,
gradientIndex,
}}
>
{children}

View file

@ -238,7 +238,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function hardReset() {
newNodeId.current = uid();
setTabId("");
setFlows([]);
setIsLoading(true);
setId(uid());

View file

@ -33,7 +33,10 @@ function ApiInterceptor() {
}
const res = await renewAccessToken(refreshToken);
login(res?.data?.access_token, res?.data?.refresh_token);
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token, res?.data?.refresh_token);
}
try {
if (error?.config?.headers) {
delete error.config.headers["Authorization"];

View file

@ -6,6 +6,8 @@ import {
APIObjectType,
LoginType,
Users,
changeUser,
resetPasswordType,
sendAllProps,
} from "../../types/api/index";
import { UserInputType } from "../../types/components";
@ -393,16 +395,18 @@ export async function autoLogin() {
export async function renewAccessToken(token: string) {
try {
return await api.post(`${BASE_URL_API}refresh?token=${token}`);
if (token) {
return await api.post(`${BASE_URL_API}refresh?token=${token}`);
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getLoggedUser(): Promise<Users> {
export async function getLoggedUser(): Promise<Users | null> {
try {
const res = await api.get(`${BASE_URL_API}user`);
const res = await api.get(`${BASE_URL_API}users/whoami`);
if (res.status === 200) {
return res.data;
@ -411,14 +415,16 @@ export async function getLoggedUser(): Promise<Users> {
console.log("Error:", error);
throw error;
}
return null;
}
export async function addUser(user: UserInputType): Promise<Users> {
export async function addUser(user: UserInputType): Promise<Array<Users>> {
try {
const res = await api.post(`${BASE_URL_API}user`, user);
if (res.status === 200) {
return res.data;
const res = await api.post(`${BASE_URL_API}users/`, user);
if (res.status !== 201) {
throw new Error(res.data.detail);
}
return res.data;
} catch (error) {
console.log("Error:", error);
throw error;
@ -428,10 +434,10 @@ export async function addUser(user: UserInputType): Promise<Users> {
export async function getUsersPage(
skip: number,
limit: number
): Promise<[Users]> {
): Promise<Array<Users>> {
try {
const res = await api.get(
`${BASE_URL_API}users?skip=${skip}&limit=${limit}`
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
);
if (res.status === 200) {
return res.data;
@ -440,11 +446,12 @@ export async function getUsersPage(
console.log("Error:", error);
throw error;
}
return [];
}
export async function deleteUser(user_id: string) {
try {
const res = await api.delete(`${BASE_URL_API}user/${user_id}`);
const res = await api.delete(`${BASE_URL_API}users/${user_id}`);
if (res.status === 200) {
return res.data;
}
@ -454,9 +461,24 @@ export async function deleteUser(user_id: string) {
}
}
export async function updateUser(user_id: string, user: Users) {
export async function updateUser(user_id: string, user: changeUser) {
try {
const res = await api.patch(`${BASE_URL_API}user/${user_id}`, user);
const res = await api.patch(`${BASE_URL_API}users/${user_id}`, user);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function resetPassword(user_id: string, user: resetPasswordType) {
try {
const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`,
user
);
if (res.status === 200) {
return res.data;
}

View file

@ -1,22 +1,39 @@
import { Infinity } from "lucide-react";
import { forwardRef } from "react";
const GradientSparkles = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<Infinity stroke="url(#grad1)" ref={ref} {...props} />
</>
);
}
);
export default GradientSparkles;
export const GradientSparkles = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
{/* this svg comes from the source code of lucide,
we do not use the import because it crashes the ui (why? no one knows...).
source code from the used svg:
https://github.com/lucide-icons/lucide/blob/a4076db69b52ff0debc383f76d4d671c3bad5345/icons/infinity.svg?short_path=f79de91
*/}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
stroke="url(#grad1)"
ref={ref}
{...props}
>
<path d="M12 12c-2-2.67-4-4-6-4a4 4 0 1 0 0 8c2 0 4-1.33 6-4Zm0 0c2 2.67 4 4 6 4a4 4 0 0 0 0-8c-2 0-4 1.33-6 4Z" />
</svg>
</>
);
});

View file

@ -33,11 +33,9 @@ const ApiModal = forwardRef(
{
flow,
children,
disable,
}: {
flow: FlowType;
children: ReactNode;
disable: boolean;
},
ref
) => {
@ -201,8 +199,8 @@ const ApiModal = forwardRef(
}
return (
<BaseModal open={open} setOpen={setOpen} disable={disable}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">Code</span>
<IconComponent

View file

@ -6,6 +6,7 @@ import BaseModal from "../baseModal";
export default function ConfirmationModal({
title,
asChild,
titleHeader,
modalContent,
modalContentTitle,
@ -22,7 +23,7 @@ export default function ConfirmationModal({
const [open, setOpen] = useState(false);
return (
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}>
<span className="pr-2">{title}</span>
<Icon

View file

@ -23,6 +23,7 @@ export default function UserManagementModal({
data,
index,
onConfirm,
asChild,
}: UserManagementType) {
const Icon: any = nodeIconsLucide[icon];
const [pwdVisible, setPwdVisible] = useState(false);
@ -60,7 +61,7 @@ export default function UserManagementModal({
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}>
<span className="pr-2">{title}</span>
<Icon

View file

@ -14,13 +14,25 @@ import { modalHeaderType } from "../../types/components";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };
type FooterProps = { children: ReactNode };
type TriggerProps = { children: ReactNode };
type TriggerProps = {
children: ReactNode;
asChild?: boolean;
disable?: boolean;
};
const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="h-full w-full">{children}</div>;
};
const Trigger: React.FC<ContentProps> = ({ children }) => {
return <>{children}</>;
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
return (
<DialogTrigger
className={asChild ? "" : "w-full"}
hidden={children ? false : true}
asChild={asChild}
>
{children}
</DialogTrigger>
);
};
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
@ -47,7 +59,6 @@ interface BaseModalProps {
];
open?: boolean;
setOpen?: (open: boolean) => void;
disable?: boolean;
size?:
| "x-small"
| "smaller"
@ -61,7 +72,6 @@ interface BaseModalProps {
function BaseModal({
open,
setOpen,
disable = false,
children,
size = "large",
}: BaseModalProps) {
@ -120,17 +130,12 @@ function BaseModal({
//UPDATE COLORS AND STYLE CLASSSES
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
className={"w-full " + (disable ? "button-disable" : "")}
hidden={triggerChild ? false : true}
>
{triggerChild}
</DialogTrigger>
{triggerChild}
<DialogContent className={minWidth}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div className={`mt-2 flex flex-col ${height} w-full `}>
<div className={`mt-2 flex flex-col ${height!} w-full `}>
{ContentChild}
</div>
{ContentFooter && (

View file

@ -28,8 +28,7 @@ export default function CodeAreaModal({
const { dark } = useContext(darkContext);
const { reactFlowInstance } = useContext(typesContext);
const [height, setHeight] = useState<string | null>(null);
const { setErrorData, setSuccessData } =
useContext(alertContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [error, setError] = useState<{
detail: { error: string | undefined; traceback: string | undefined };
} | null>(null);

View file

@ -10,15 +10,15 @@ import BaseModal from "../baseModal";
const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, updateFlow, downloadFlow } = useContext(TabsContext);
const { flows, tabId, downloadFlow } = useContext(TabsContext);
const [checked, setChecked] = useState(false);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow.name);
setDescription(flow.description);
}, [flow.name, flow.description]);
const [name, setName] = useState(flow.name);
const [description, setDescription] = useState(flow.description);
setName(flow!.name);
setDescription(flow!.description);
}, [flow!.name, flow!.description]);
const [name, setName] = useState(flow!.name);
const [description, setDescription] = useState(flow!.description);
const [open, setOpen] = useState(false);
return (
@ -40,7 +40,6 @@ const ExportModal = forwardRef(
tabId={tabId}
setName={setName}
setDescription={setDescription}
updateFlow={updateFlow}
/>
<div className="mt-3 flex items-center space-x-2">
<Checkbox

View file

@ -194,7 +194,8 @@ export default function FormModal({
}
function handleWsMessage(data: any) {
if (Array.isArray(data)) {
console.log(data);
if (Array.isArray(data) && data.length > 0) {
//set chat history
setChatHistory((_) => {
let newChatHistory: ChatMessageType[] = [];
@ -313,7 +314,7 @@ export default function FormModal({
}
};
// do not add connectWS on dependencies array
}, []);
}, [open]);
useEffect(() => {
if (

View file

@ -131,6 +131,7 @@ export default function GenericModal({
setNoticeData({
title: "Your template does not have any variables.",
});
setModalOpen(false);
} else {
setIsEdit(false);
setSuccessData({

View file

@ -48,7 +48,7 @@ export default function LoginAdminPage() {
}
function getUser() {
if (getAuthentication) {
if (getAuthentication()) {
setTimeout(() => {
getLoggedUser()
.then((user) => {

View file

@ -1,10 +1,10 @@
import { cloneDeep } from "lodash";
import { X } from "lucide-react";
import { useContext, useEffect, useRef, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import LoadingComponent from "../../components/loadingComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { Input } from "../../components/ui/input";
@ -16,8 +16,13 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
ADMIN_HEADER_DESCRIPTION,
ADMIN_HEADER_TITLE,
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import {
addUser,
deleteUser,
@ -33,12 +38,19 @@ export default function AdminPage() {
const [inputValue, setInputValue] = useState("");
const [size, setPageSize] = useState(10);
const [index, setPageIndex] = useState(0);
const [index, setPageIndex] = useState(1);
const [loadingUsers, setLoadingUsers] = useState(true);
const { setErrorData, setSuccessData } = useContext(alertContext);
const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const userList = useRef([]);
useEffect(() => {
@ -65,7 +77,9 @@ export default function AdminPage() {
function handleChangePagination(pageIndex: number, pageSize: number) {
setLoadingUsers(true);
getUsersPage(pageIndex, pageSize)
setPageSize(pageSize);
setPageIndex(pageIndex);
getUsersPage(pageSize * (pageIndex - 1), pageSize)
.then((users) => {
setTotalRowsCount(users["total_count"]);
userList.current = users["users"];
@ -78,7 +92,7 @@ export default function AdminPage() {
}
function resetFilter() {
setPageIndex(0);
setPageIndex(1);
setPageSize(10);
getUsers();
}
@ -168,270 +182,264 @@ export default function AdminPage() {
function handleNewUser(user: UserInputType) {
addUser(user)
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! New user added!",
updateUser(res["id"], {
is_active: user.is_active,
is_superuser: user.is_superuser,
}).then((res) => {
resetFilter();
setSuccessData({
title: "Success! New user added!",
});
});
})
.catch((error) => {
setErrorData({
title: "Error on add new user",
list: [error["response"]["data"]["detail"]],
title: "Error when adding new user",
list: [error.response.data.detail],
});
});
}
return (
<>
<div className="flex flex-col">
<Header />
{userData && (
<div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">
Welcome back!
</h2>
<p className="text-muted-foreground">
Navigate through this section to efficiently oversee all
application users. From here, you can seamlessly manage
user accounts.
</p>
</div>
<div className="flex items-center space-x-2"></div>
</div>
{userList.current.length === 0 && !loadingUsers && (
<>
<div className="flex items-center justify-between">
<h2>There's no users registered :)</h2>
</div>
</>
)}
<>
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2">
<Input
value={inputValue}
placeholder="Filter users..."
className="h-8 w-[150px] lg:w-[250px]"
onChange={(e) => handleFilterUsers(e.target.value)}
/>
{inputValue.length > 0 && (
<Button
onClick={() => {
setInputValue("");
setFilterUserList(userList.current);
}}
variant="ghost"
className="h-8 px-2 lg:px-3"
>
Reset
<X className="ml-2 h-4 w-4" />
</Button>
)}
</div>
<div>
<UserManagementModal
title="New User"
titleHeader={"Add a new user"}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
onConfirm={(index, user) => {
handleNewUser(user);
}}
>
<Button>New User</Button>
</UserManagementModal>
</div>
</div>
{loadingUsers && (
<div>
<strong>Loading...</strong>
</div>
)}
<div
className={
"max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
(loadingUsers ? " border-0" : "")
}
>
<Table className={"table-fixed bg-muted outline-1"}>
<TableHeader
className={
loadingUsers
? "hidden"
: "table-fixed bg-muted outline-1"
}
>
<TableRow>
<TableHead className="h-10">Id</TableHead>
<TableHead className="h-10">Username</TableHead>
<TableHead className="h-10">Active</TableHead>
<TableHead className="h-10">Superuser</TableHead>
<TableHead className="h-10">Created At</TableHead>
<TableHead className="h-10">Updated At</TableHead>
<TableHead className="h-10 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
{!loadingUsers && (
<TableBody>
{filterUserList.map(
(user: UserInputType, index) => (
<TableRow key={index}>
<TableCell className="truncate py-2 font-medium">
<ShadTooltip content={user.id}>
<span className="cursor-default">
{user.id}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<ShadTooltip content={user.username}>
<span className="cursor-default">
{user.username}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDisableUser(
user.is_active,
user.id,
user
);
}}
>
<Checkbox
id="is_active"
checked={user.is_active}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleSuperUserEdit(
user.is_superuser,
user.id,
user
);
}}
>
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
{
new Date(user.create_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="truncate py-2">
{
new Date(user.updated_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<UserManagementModal
title="Edit"
titleHeader={`${user.id}`}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
data={user}
index={index}
onConfirm={(index, editUser) => {
handleEditUser(user.id, editUser);
}}
>
<ShadTooltip content="Edit" side="top">
<IconComponent
name="Pencil"
className="h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</UserManagementModal>
<ConfirmationModal
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDeleteUser(user);
}}
>
<ShadTooltip
content="Delete"
side="top"
>
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
)
)}
</TableBody>
)}
</Table>
</div>
<PaginatorComponent
pageIndex={index}
pageSize={size}
totalRowsCount={totalRowsCount}
paginate={(pageIndex, pageSize) => {
handleChangePagination(pageSize, pageIndex);
}}
></PaginatorComponent>
</>
<Header />
{userData && (
<div className="admin-page-panel flex h-full flex-col pb-8">
<div className="main-page-nav-arrangement">
<span className="main-page-nav-title">
<IconComponent name="Shield" className="w-6" />
{ADMIN_HEADER_TITLE}
</span>
</div>
<span className="admin-page-description-text">
{ADMIN_HEADER_DESCRIPTION}
</span>
<div className="flex w-full justify-between px-4">
<div className="flex w-96 items-center gap-4">
<Input
placeholder="Search Username"
value={inputValue}
onChange={(e) => handleFilterUsers(e.target.value)}
/>
{inputValue.length > 0 ? (
<div
className="cursor-pointer"
onClick={() => {
setInputValue("");
setFilterUserList(userList.current);
}}
>
<IconComponent name="X" className="w-6 text-foreground" />
</div>
</div>
) : (
<div>
<IconComponent
name="Search"
className="w-6 text-foreground"
/>
</div>
)}
</div>
<div>
<UserManagementModal
title="New User"
titleHeader={"Add a new user"}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
onConfirm={(index, user) => {
handleNewUser(user);
}}
asChild
>
<Button variant="primary">New User</Button>
</UserManagementModal>
</div>
</div>
)}
</div>
{loadingUsers ? (
<div className="flex h-full w-full items-center justify-center">
<LoadingComponent remSize={12} />
</div>
) : userList.current.length === 0 ? (
<>
<div className="m-4 flex items-center justify-between text-sm">
No users registered.
</div>
</>
) : (
<>
<div
className={
"m-4 h-full overflow-x-hidden overflow-y-scroll rounded-md border-2 bg-background custom-scroll" +
(loadingUsers ? " border-0" : "")
}
>
<Table className={"table-fixed outline-1 "}>
<TableHeader
className={
loadingUsers ? "hidden" : "table-fixed bg-muted outline-1"
}
>
<TableRow>
<TableHead className="h-10">Id</TableHead>
<TableHead className="h-10">Username</TableHead>
<TableHead className="h-10">Active</TableHead>
<TableHead className="h-10">Superuser</TableHead>
<TableHead className="h-10">Created At</TableHead>
<TableHead className="h-10">Updated At</TableHead>
<TableHead className="h-10 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
{!loadingUsers && (
<TableBody>
{filterUserList.map((user: UserInputType, index) => (
<TableRow key={index}>
<TableCell className="truncate py-2 font-medium">
<ShadTooltip content={user.id}>
<span className="cursor-default">{user.id}</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<ShadTooltip content={user.username}>
<span className="cursor-default">
{user.username}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
<ConfirmationModal
asChild
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDisableUser(
user.is_active,
user.id,
user
);
}}
>
<div className="flex w-fit">
<Checkbox
id="is_active"
checked={user.is_active}
/>
</div>
</ConfirmationModal>
</TableCell>
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
<ConfirmationModal
asChild
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleSuperUserEdit(
user.is_superuser,
user.id,
user
);
}}
>
<div className="flex w-fit">
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</div>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
{
new Date(user.create_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="truncate py-2">
{
new Date(user.updated_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<UserManagementModal
title="Edit"
titleHeader={`${user.id}`}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
data={user}
index={index}
onConfirm={(index, editUser) => {
handleEditUser(user.id, editUser);
}}
>
<ShadTooltip content="Edit" side="top">
<IconComponent
name="Pencil"
className="h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</UserManagementModal>
<ConfirmationModal
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDeleteUser(user);
}}
>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
)}
</Table>
</div>
<PaginatorComponent
pageIndex={index}
pageSize={size}
totalRowsCount={totalRowsCount}
paginate={(pageSize, pageIndex) => {
handleChangePagination(pageIndex, pageSize);
}}
></PaginatorComponent>
</>
)}
</div>
)}
</>
);
}

View file

@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
export default function CommunityPage(): JSX.Element {
@ -74,7 +75,14 @@ export default function CommunityPage(): JSX.Element {
new and powerful features.
</span>
<div className="community-pages-flows-panel">
{!loadingExamples &&
{loadingExamples ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
examples.map((flow, idx) => (
<CardComponent
key={idx}
@ -99,7 +107,8 @@ export default function CommunityPage(): JSX.Element {
</Button>
}
/>
))}
))
)}
</div>
</div>
</>

View file

@ -298,7 +298,14 @@ export default function Page({
setNodes((nds) => nds.concat(newNode));
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
uploadFlow(false, event.dataTransfer.files.item(0)!);
if (event.dataTransfer.files.item(0)!.type === "application/json") {
uploadFlow(true, event.dataTransfer.files.item(0)!);
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
},
// Specify dependencies for useCallback

View file

@ -99,16 +99,20 @@ export default function ExtraSidebar(): JSX.Element {
<ShadTooltip content={"Code"} side="top">
<div className="side-bar-button">
{flow && flow.data && (
<ApiModal flow={flow} disable={!isBuilt}>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="Code2"
className={
"side-bar-button-size" +
(isBuilt ? " " : " extra-side-bar-save-disable")
}
/>
</div>
<ApiModal flow={flow}>
<button
className={"w-full " + (!isBuilt ? "button-disable" : "")}
>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="Code2"
className={
"side-bar-button-size" +
(isBuilt ? " " : " extra-side-bar-save-disable")
}
/>
</div>
</button>
</ApiModal>
)}
</div>

View file

@ -1,4 +1,4 @@
import { useContext, useEffect } from "react";
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import DropdownButton from "../../components/DropdownButtonComponent";
import { CardComponent } from "../../components/cardComponent";
@ -7,6 +7,7 @@ import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
export default function HomePage(): JSX.Element {
const {
@ -19,6 +20,7 @@ export default function HomePage(): JSX.Element {
uploadFlow,
isLoading,
} = useContext(TabsContext);
const { setErrorData } = useContext(alertContext);
const dropdownOptions = [
{
name: "Import from JSON",
@ -35,9 +37,36 @@ export default function HomePage(): JSX.Element {
}, []);
const navigate = useNavigate();
useEffect(() => {
console.log(isLoading);
}, [isLoading]);
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const dragEnter = (e) => {
e.preventDefault();
setIsDragging(true);
};
const dragLeave = () => {
setIsDragging(false);
};
const fileDrop = (e) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow(true, e.dataTransfer.files.item(0)!);
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
};
// Personal flows display
return (
@ -82,40 +111,63 @@ export default function HomePage(): JSX.Element {
<span className="main-page-description-text">
Manage your personal projects. Download or upload your collection.
</span>
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={fileDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
>
{isDragging ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<IconComponent
name="ArrowUpToLine"
className="h-12 w-12 stroke-1"
/>
Drop your flow here
</>
) : (
flows.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Link to={"/flow/" + flow.id}>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
flows.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Link to={"/flow/" + flow.id}>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
)}
</div>
)}
</div>
</div>

View file

@ -0,0 +1,173 @@
import * as Form from "@radix-ui/react-form";
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import GradientChooserComponent from "../../components/gradientChooserComponent";
import Header from "../../components/headerComponent";
import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button";
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { resetPassword, updateUser } from "../../controllers/API";
import {
inputHandlerEventType,
patchUserInputStateType,
} from "../../types/components";
import { gradients } from "../../utils/styleUtils";
export default function ProfileSettingsPage(): JSX.Element {
const { setTabId } = useContext(TabsContext);
const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE
);
// set null id
useEffect(() => {
setTabId("");
}, []);
const { setErrorData, setSuccessData } = useContext(alertContext);
const { userData, setUserData } = useContext(AuthContext);
const { password, cnfPassword, gradient } = inputState;
async function handlePatchUser() {
if (password !== cnfPassword) {
setErrorData({
title: "Error changing password",
list: ["Passwords do not match"],
});
return;
}
try {
if (password !== "") await resetPassword(userData!.id, { password });
if (gradient !== "")
await updateUser(userData!.id, { profile_image: gradient });
if (gradient !== "") {
let newUserData = cloneDeep(userData);
newUserData!.profile_image = gradient;
setUserData(newUserData);
}
handleInput({ target: { name: "password", value: "" } });
handleInput({ target: { name: "cnfPassword", value: "" } });
setSuccessData({ title: "Changes saved successfully!" });
} catch (error) {
setErrorData({
title: "Error saving changes",
list: [(error as any).response.data.detail],
});
}
}
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
return (
<>
<Header />
<div className="community-page-arrangement">
<div className="community-page-nav-arrangement">
<span className="community-page-nav-title">
<IconComponent name="User" className="w-6" />
Profile Settings
</span>
</div>
<span className="community-page-description-text">
Change your profile settings like your password and your profile
picture.
</span>
<Form.Root
onSubmit={(event) => {
handlePatchUser();
const data = Object.fromEntries(new FormData(event.currentTarget));
event.preventDefault();
}}
className="flex h-full flex-col px-6 pb-16"
>
<div className="flex h-full flex-col gap-4">
<div className="flex gap-4">
<div className="mb-3 w-96">
<Form.Field name="password">
<Form.Label className="data-[invalid]:label-invalid">
Password{" "}
</Form.Label>
<InputComponent
onChange={(value) => {
handleInput({ target: { name: "password", value } });
}}
value={password}
isForm
password={true}
placeholder="Password"
className="w-full"
/>
<Form.Message match="valueMissing" className="field-invalid">
Please enter your password
</Form.Message>
</Form.Field>
</div>
<div className="mb-3 w-96">
<Form.Field name="cnfPassword">
<Form.Label className="data-[invalid]:label-invalid">
Confirm Password{" "}
</Form.Label>
<InputComponent
onChange={(value) => {
handleInput({ target: { name: "cnfPassword", value } });
}}
value={cnfPassword}
isForm
password={true}
placeholder="Confirm Password"
className="w-full"
/>
<Form.Message className="field-invalid" match="valueMissing">
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</div>
<Form.Field name="gradient">
<Form.Label className="data-[invalid]:label-invalid">
Profile Gradient{" "}
</Form.Label>
<div className="mt-4 w-[1010px]">
<GradientChooserComponent
value={
gradient == ""
? userData!.profile_image ??
gradients[
parseInt(userData!.id ?? "", 30) % gradients.length
]
: gradient
}
onChange={(value) => {
handleInput({ target: { name: "gradient", value } });
}}
/>
</div>
</Form.Field>
</div>
<div className="flex w-full justify-end">
<div className="w-32">
<Form.Submit asChild>
<Button className="mr-3 mt-6 w-full" type="submit">
Save
</Button>
</Form.Submit>
</div>
</div>
</Form.Root>
</div>
</>
);
}

View file

@ -54,7 +54,7 @@ export default function LoginPage(): JSX.Element {
setTimeout(() => {
getLoggedUser()
.then((user) => {
const isSuperUser = user.is_superuser;
const isSuperUser = user!.is_superuser;
setIsAdmin(isSuperUser);
setUserData(user);
})

View file

@ -9,6 +9,7 @@ import ApiKeysPage from "./pages/ApiKeysPage";
import CommunityPage from "./pages/CommunityPage";
import FlowPage from "./pages/FlowPage";
import HomePage from "./pages/MainPage";
import ProfileSettingsPage from "./pages/ProfileSettingsPage";
import ViewPage from "./pages/ViewPage";
import DeleteAccountPage from "./pages/deleteAccountPage";
import LoginPage from "./pages/loginPage";
@ -95,6 +96,14 @@ const Router = () => {
/>
<Route path="/account">
<Route
path="settings"
element={
<ProtectedRoute>
<ProfileSettingsPage />
</ProtectedRoute>
}
/>
<Route
path="delete"
element={

View file

@ -212,6 +212,10 @@
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
}
.admin-page-panel {
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
}
.main-page-nav-arrangement {
@apply flex-max-width justify-between px-6 py-12 pb-2;
}
@ -228,6 +232,10 @@
@apply flex w-[60%] px-6 pb-14 text-muted-foreground;
}
.admin-page-description-text {
@apply flex w-[80%] px-6 pb-8 text-muted-foreground;
}
.main-page-flows-display {
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
}
@ -303,7 +311,7 @@
@apply hover:text-accent-foreground hover:transition-all;
}
.generic-node-desc {
@apply h-full w-full py-5 text-foreground;
@apply h-full w-full text-foreground;
}
.generic-node-desc-text {
@apply w-full px-5 pb-3 text-sm text-muted-foreground;

View file

@ -78,11 +78,25 @@ export type LoginAuthType = {
token_type?: string;
};
export type changeUser = {
username?: string;
is_active?: boolean;
is_superuser?: boolean;
password?: string;
profile_image?: string;
};
export type resetPasswordType = {
password?: string;
profile_image?: string;
};
export type Users = {
id: string;
username: string;
is_active: boolean;
is_superuser: boolean;
profile_image: string;
create_at: Date;
updated_at: Date;
};

View file

@ -248,6 +248,7 @@ export type PaginatorComponentType = {
export type ConfirmationModalType = {
title: string;
titleHeader: string;
asChild?: boolean;
modalContent: string;
modalContentTitle: string;
cancelText: string;
@ -268,6 +269,7 @@ export type UserManagementType = {
icon: string;
data?: any;
index?: number;
asChild?: boolean;
onConfirm: (index, data) => void;
};
@ -276,6 +278,12 @@ export type loginInputStateType = {
password: string;
};
export type patchUserInputStateType = {
password: string;
cnfPassword: string;
gradient: string;
};
export type UserInputType = {
username: string;
password: string;

View file

@ -48,6 +48,8 @@ export type darkContextType = {
setDark: (newState: {}) => void;
stars: number;
setStars: (stars: number) => void;
gradientIndex: number;
setGradientIndex: (index: number) => void;
};
export type locationContextType = {

View file

@ -254,7 +254,9 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
}
export function handleKeyDown(
e: React.KeyboardEvent<HTMLInputElement>,
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>,
inputValue: string | string[] | null,
block: string
) {

View file

@ -1,4 +1,5 @@
import {
ArrowUpToLine,
Bell,
Check,
CheckCircle2,
@ -58,6 +59,7 @@ import {
Scissors,
Search,
Settings2,
Shield,
Sparkles,
SunIcon,
TerminalSquare,
@ -65,6 +67,7 @@ import {
Undo,
Unplug,
Upload,
User,
UserCog2,
UserMinus2,
UserPlus2,
@ -87,7 +90,7 @@ import { EvernoteIcon } from "../icons/Evernote";
import { FBIcon } from "../icons/FacebookMessenger";
import { GitBookIcon } from "../icons/GitBook";
import { GoogleIcon } from "../icons/Google";
import GradientSparkles from "../icons/GradientSparkles";
import { GradientSparkles } from "../icons/GradientSparkles";
import { HuggingFaceIcon } from "../icons/HuggingFace";
import { IFixIcon } from "../icons/IFixIt";
import { MetaIcon } from "../icons/Meta";
@ -187,6 +190,7 @@ export const nodeNames: { [char: string]: string } = {
};
export const nodeIconsLucide: iconsType = {
ArrowUpToLine: ArrowUpToLine,
Chroma: ChromaIcon,
AirbyteJSONLoader: AirbyteIcon,
Anthropic: AnthropicIcon,
@ -221,6 +225,7 @@ export const nodeIconsLucide: iconsType = {
ChatVertexAI: VertexAIIcon,
VertexAIEmbeddings: VertexAIIcon,
agents: Rocket,
User,
WikipediaAPIWrapper: SvgWikipedia,
chains: Link,
memories: Cpu,
@ -261,6 +266,7 @@ export const nodeIconsLucide: iconsType = {
Bell,
ChevronLeft,
ChevronDown,
Shield,
Plus,
Redo,
Settings2,

View file

@ -166,7 +166,8 @@ def test_user(client):
username="testuser",
password="testpassword",
)
response = client.post("/api/v1/user", json=user_data.dict())
response = client.post("/api/v1/users", json=user_data.dict())
assert response.status_code == 201
return response.json()

View file

@ -78,24 +78,27 @@ def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_head
assert response.json()["detail"] == "The user doesn't have enough privileges"
def test_data_consistency_after_update(client, active_user, logged_in_headers):
def test_data_consistency_after_update(
client, active_user, logged_in_headers, super_user_headers
):
user_id = active_user.id
update_data = UserUpdate(username="newname")
update_data = UserUpdate(is_active=False)
response = client.patch(
f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=super_user_headers
)
assert response.status_code == 200
assert response.status_code == 200, response.json()
# Fetch the updated user from the database
response = client.get("/api/v1/user", headers=logged_in_headers)
assert response.json()["username"] == "newname", response.json()
response = client.get("/api/v1/users/whoami", headers=logged_in_headers)
assert response.status_code == 401, response.json()
assert response.json()["detail"] == "Could not validate credentials"
def test_data_consistency_after_delete(client, test_user, super_user_headers):
user_id = test_user["id"]
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
assert response.status_code == 200
user_id = test_user.get("id")
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
assert response.status_code == 200, response.json()
# Attempt to fetch the deleted user from the database
response = client.get("/api/v1/users", headers=super_user_headers)
@ -157,11 +160,37 @@ def test_patch_user(client, active_user, logged_in_headers):
)
response = client.patch(
f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
)
assert response.status_code == 304, response.json()
update_data = UserUpdate(
profile_image="new_image",
)
response = client.patch(
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
)
assert response.status_code == 200, response.json()
def test_patch_reset_password(client, active_user, logged_in_headers):
user_id = active_user.id
update_data = UserUpdate(
password="newpassword",
)
response = client.patch(
f"/api/v1/users/{user_id}/reset-password",
json=update_data.dict(),
headers=logged_in_headers,
)
assert response.status_code == 200, response.json()
# Now we need to test if the new password works
login_data = {"username": active_user.username, "password": "newpassword"}
response = client.post("/api/v1/login", data=login_data)
assert response.status_code == 200
def test_patch_user_wrong_id(client, active_user, logged_in_headers):
user_id = "wrong_id"
update_data = UserUpdate(
@ -169,7 +198,7 @@ def test_patch_user_wrong_id(client, active_user, logged_in_headers):
)
response = client.patch(
f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
)
assert response.status_code == 422, response.json()
assert response.json() == {
@ -185,14 +214,14 @@ def test_patch_user_wrong_id(client, active_user, logged_in_headers):
def test_delete_user(client, test_user, super_user_headers):
user_id = test_user["id"]
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
assert response.status_code == 200
assert response.json() == {"detail": "User deleted"}
def test_delete_user_wrong_id(client, test_user, super_user_headers):
user_id = "wrong_id"
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
assert response.status_code == 422
assert response.json() == {
"detail": [
@ -207,13 +236,6 @@ def test_delete_user_wrong_id(client, test_user, super_user_headers):
def test_normal_user_cant_delete_user(client, test_user, logged_in_headers):
user_id = test_user["id"]
response = client.delete(f"/api/v1/user/{user_id}", headers=logged_in_headers)
response = client.delete(f"/api/v1/users/{user_id}", headers=logged_in_headers)
assert response.status_code == 400
assert response.json() == {"detail": "The user doesn't have enough privileges"}
# If you still want to test the superuser endpoint
def test_add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(client):
response = client.post("/api/v1/super_user")
assert response.status_code == 200
assert response.json()["username"] == "superuser"