Merge remote-tracking branch 'origin/dev' into celery

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-09-22 12:53:05 -03:00
commit 78ecb4c341
123 changed files with 4453 additions and 8987 deletions

View file

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

View file

@ -41,10 +41,10 @@ run_frontend:
cd src/frontend && npm start cd src/frontend && npm start
run_cli: run_cli:
poetry run langflow --path src/frontend/build poetry run langflow run --path src/frontend/build
run_cli_debug: 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: setup_devcontainer:
make init make init
@ -69,7 +69,7 @@ backend:
build_and_run: build_and_run:
echo 'Removing dist folder' echo 'Removing dist folder'
rm -rf dist 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: build_and_install:
echo 'Removing dist folder' 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
[![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 ## Deploy on Render

View file

@ -33,6 +33,7 @@ The CustomComponent class serves as the foundation for creating custom component
| Supported Types | | Supported Types |
| --------------------------------------------------------- | | --------------------------------------------------------- |
| _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ | | _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ |
| _`langflow.field_typing.NestedDict`_ |
| _`langchain.chains.base.Chain`_ | | _`langchain.chains.base.Chain`_ |
| _`langchain.PromptTemplate`_ | | _`langchain.PromptTemplate`_ |
| _`langchain.llms.base.BaseLLM`_ | | _`langchain.llms.base.BaseLLM`_ |
@ -44,6 +45,8 @@ The CustomComponent class serves as the foundation for creating custom component
| _`langchain.embeddings.base.Embeddings`_ | | _`langchain.embeddings.base.Embeddings`_ |
| _`langchain.schema.BaseRetriever`_ | | _`langchain.schema.BaseRetriever`_ |
The difference between _`dict`_ and _`langflow.field_typing.NestedDict`_ is that one adds a simple key-value pair field, while the other opens a more robust dictionary editor.
<Admonition type="info"> <Admonition type="info">
Unlike Langchain types, base Python types do not add a Unlike Langchain types, base Python types do not add a
[handle](../guidelines/components) to the field by default. To add handles, [handle](../guidelines/components) to the field by default. To add handles,

View file

@ -1,10 +1,12 @@
import Admonition from '@theme/Admonition'; import Admonition from "@theme/Admonition";
# Text Splitters # Text Splitters
<Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION"> <Admonition type="caution" icon="🚧" title="ZONE UNDER CONSTRUCTION">
<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! 🛠️📝 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>
</Admonition> </Admonition>
@ -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. - **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

@ -387,7 +387,7 @@ Your structure should look something like this:
The recommended way to load custom components is to set the _`LANGFLOW_COMPONENTS_PATH`_ environment variable to the path of your custom components directory. Then, run the Langflow CLI as usual. The recommended way to load custom components is to set the _`LANGFLOW_COMPONENTS_PATH`_ environment variable to the path of your custom components directory. Then, run the Langflow CLI as usual.
```bash ```bash
export LANGFLOW_COMPONENTS_PATH=/path/to/components export LANGFLOW_COMPONENTS_PATH='["/path/to/components"]'
langflow langflow
``` ```

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", type: "category",
label: "Step-by-Step Guides", label: "Step-by-Step Guides",
collapsed: false, collapsed: false,
items: ["guides/loading_document", "guides/chatprompttemplate_guide"], items: [
"guides/loading_document",
"guides/chatprompttemplate_guide",
"guides/langfuse_integration",
],
}, },
// { // {
// type: 'category', // type: 'category',

1022
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "0.5.0a0" version = "0.5.0a1"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"] authors = ["Logspace <contact@logspace.ai>"]
maintainers = [ maintainers = [
@ -32,17 +32,17 @@ beautifulsoup4 = "^4.12.2"
google-search-results = "^2.4.1" google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0" google-api-python-client = "^2.79.0"
typer = "^0.9.0" typer = "^0.9.0"
gunicorn = "^21.1.0" gunicorn = "^21.2.0"
langchain = "^0.0.274" langchain = "^0.0.274"
openai = "^0.27.8" openai = "^0.27.8"
pandas = "2.0.3" pandas = "2.0.3"
chromadb = "^0.3.21" chromadb = "^0.3.21"
huggingface-hub = { version = "^0.16.0", extras = ["inference"] } 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 } llama-cpp-python = { version = "~0.1.0", optional = true }
networkx = "^3.1" networkx = "^3.1"
unstructured = "^0.7.0" unstructured = "^0.10.0"
pypdf = "^3.11.0" pypdf = "^3.15.0"
lxml = "^4.9.2" lxml = "^4.9.2"
pysrt = "^1.1.2" pysrt = "^1.1.2"
fake-useragent = "^1.2.1" fake-useragent = "^1.2.1"
@ -52,13 +52,13 @@ pyarrow = "^12.0.0"
tiktoken = "~0.4.0" tiktoken = "~0.4.0"
wikipedia = "^1.4.0" wikipedia = "^1.4.0"
langchain-serve = { version = ">0.0.51", optional = true } langchain-serve = { version = ">0.0.51", optional = true }
qdrant-client = "^1.3.0" qdrant-client = "^1.4.0"
websockets = "^10.3" websockets = "^10.3"
weaviate-client = "^3.21.0" weaviate-client = "^3.23.0"
jina = "3.15.2" jina = "3.15.2"
sentence-transformers = { version = "^2.2.2", optional = true } sentence-transformers = { version = "^2.2.2", optional = true }
ctransformers = { version = "^0.2.10", optional = true } ctransformers = { version = "^0.2.10", optional = true }
cohere = "^4.11.0" cohere = "^4.21.0"
python-multipart = "^0.0.6" python-multipart = "^0.0.6"
sqlmodel = "^0.0.8" sqlmodel = "^0.0.8"
faiss-cpu = "^1.7.4" faiss-cpu = "^1.7.4"
@ -85,12 +85,16 @@ passlib = "^1.7.4"
bcrypt = "^4.0.1" bcrypt = "^4.0.1"
python-jose = "^3.3.0" python-jose = "^3.3.0"
metaphor-python = "^0.1.11" metaphor-python = "^0.1.11"
markupsafe = "^2.1.3"
pywin32 = { version = "^306", markers = "sys_platform == 'win32'" } pywin32 = { version = "^306", markers = "sys_platform == 'win32'" }
loguru = "^0.7.1" 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] [tool.poetry.group.dev.dependencies]
types-redis = "^4.6.0.5"
black = "^23.1.0" black = "^23.1.0"
ipykernel = "^6.21.2" ipykernel = "^6.21.2"
mypy = "^1.1.1" mypy = "^1.1.1"
@ -110,7 +114,7 @@ locust = "^2.16.1"
pytest-mock = "^3.11.1" pytest-mock = "^3.11.1"
pytest-xdist = "^3.3.1" pytest-xdist = "^3.3.1"
types-pywin32 = "^306.0.0.4" types-pywin32 = "^306.0.0.4"
types-redis = "^4.6.0.5" types-google-cloud-ndb = "^2.2.0.0"
pytest-sugar = "^0.9.7" pytest-sugar = "^0.9.7"

View file

@ -3,9 +3,14 @@ services:
- type: web - type: web
name: langflow name: langflow
runtime: docker runtime: docker
plan: free
dockerfilePath: ./Dockerfile dockerfilePath: ./Dockerfile
repo: https://github.com/logspace-ai/langflow repo: https://github.com/logspace-ai/langflow
branch: main branch: main
healthCheckPath: /health healthCheckPath: /health
autoDeploy: false 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

@ -55,7 +55,7 @@ def display_results(results):
def update_settings( def update_settings(
config: str, config: str,
cache: str, cache: Optional[str] = None,
dev: bool = False, dev: bool = False,
remove_api_keys: bool = False, remove_api_keys: bool = False,
components_path: Optional[Path] = None, components_path: Optional[Path] = None,
@ -153,10 +153,10 @@ def run(
log_file: Path = typer.Option( log_file: Path = typer.Option(
"logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE" "logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE"
), ),
cache: str = typer.Option( cache: Optional[str] = typer.Option(
envvar="LANGFLOW_LANGCHAIN_CACHE", envvar="LANGFLOW_LANGCHAIN_CACHE",
help="Type of cache to use. (InMemoryCache, SQLiteCache)", help="Type of cache to use. (InMemoryCache, SQLiteCache)",
default="SQLiteCache", default=None,
), ),
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"), jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"), dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),

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

@ -18,15 +18,17 @@ from langflow.services.auth.utils import (
get_current_active_superuser, get_current_active_superuser,
get_current_active_user, get_current_active_user,
get_password_hash, get_password_hash,
verify_password,
) )
from langflow.services.database.models.user.crud import ( from langflow.services.database.models.user.crud import (
get_user_by_id,
update_user, 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( def add_user(
user: UserCreate, user: UserCreate,
session: Session = Depends(get_session), session: Session = Depends(get_session),
@ -50,7 +52,7 @@ def add_user(
return new_user return new_user
@router.get("/user", response_model=UserRead) @router.get("/whoami", response_model=UserRead)
def read_current_user( def read_current_user(
current_user: User = Depends(get_current_active_user), current_user: User = Depends(get_current_active_user),
) -> User: ) -> User:
@ -60,7 +62,7 @@ def read_current_user(
return current_user return current_user
@router.get("/users", response_model=UsersResponse) @router.get("/", response_model=UsersResponse)
def read_all_users( def read_all_users(
skip: int = 0, skip: int = 0,
limit: int = 10, 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( def patch_user(
user_id: UUID, user_id: UUID,
user: UserUpdate, user_update: UserUpdate,
_: Session = Depends(get_current_active_user), user: User = Depends(get_current_active_user),
session: Session = Depends(get_session), session: Session = Depends(get_session),
) -> User: ) -> User:
""" """
Update an existing user's data. 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( def delete_user(
user_id: UUID, user_id: UUID,
current_user: User = Depends(get_current_active_superuser), current_user: User = Depends(get_current_active_superuser),

View file

@ -58,6 +58,16 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
def get_old_custom_fields(prompt_request): def get_old_custom_fields(prompt_request):
try: try:
if (
len(prompt_request.frontend_node.custom_fields) == 1
and prompt_request.name == ""
):
# If there is only one custom field and the name is empty string
# then we are dealing with the first prompt request after the node was created
prompt_request.name = list(
prompt_request.frontend_node.custom_fields.keys()
)[0]
old_custom_fields = prompt_request.frontend_node.custom_fields[ old_custom_fields = prompt_request.frontend_node.custom_fields[
prompt_request.name prompt_request.name
].copy() ].copy()

View file

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

View file

@ -16,17 +16,14 @@ class PromptRunner(CustomComponent):
"info": "Make sure the prompt has all variables filled.", "info": "Make sure the prompt has all variables filled.",
}, },
"code": {"show": False}, "code": {"show": False},
"inputs": {"field_type": "code"},
} }
def build( def build(
self, self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}
llm: BaseLLM,
prompt: PromptTemplate,
) -> Document: ) -> Document:
chain = prompt | llm chain = prompt | llm
# The input is an empty dict because the prompt is already filled # The input is an empty dict because the prompt is already filled
result = chain.invoke({}) result = chain.invoke(input=inputs)
if hasattr(result, "content"): if hasattr(result, "content"):
result = result.content result = result.content
self.repr_value = result self.repr_value = result

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"},
"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

@ -19,7 +19,6 @@ class GetRequest(CustomComponent):
}, },
"headers": { "headers": {
"display_name": "Headers", "display_name": "Headers",
"field_type": "code",
"info": "The headers to send with the request.", "info": "The headers to send with the request.",
}, },
"code": {"show": False}, "code": {"show": False},

View file

@ -15,7 +15,6 @@ class PostRequest(CustomComponent):
"url": {"display_name": "URL", "info": "The URL to make the request to."}, "url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": { "headers": {
"display_name": "Headers", "display_name": "Headers",
"field_type": "code",
"info": "The headers to send with the request.", "info": "The headers to send with the request.",
}, },
"code": {"show": False}, "code": {"show": False},

View file

@ -15,7 +15,7 @@ class UpdateRequest(CustomComponent):
"url": {"display_name": "URL", "info": "The URL to make the request to."}, "url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": { "headers": {
"display_name": "Headers", "display_name": "Headers",
"field_type": "code", "field_type": "NestedDict",
"info": "The headers to send with the request.", "info": "The headers to send with the request.",
}, },
"code": {"show": False}, "code": {"show": False},

View file

@ -171,8 +171,6 @@ prompts:
textsplitters: textsplitters:
CharacterTextSplitter: CharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter" 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: toolkits:
OpenAPIToolkit: OpenAPIToolkit:
documentation: "" documentation: ""

View file

@ -0,0 +1,3 @@
from .base import NestedDict
__all__ = ["NestedDict"]

View file

@ -0,0 +1,4 @@
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]

View file

@ -195,6 +195,19 @@ class Vertex:
except Exception as exc: except Exception as exc:
logger.debug(f"Error parsing code: {exc}") logger.debug(f"Error parsing code: {exc}")
params[key] = value.get("value") params[key] = value.get("value")
elif value.get("type") in ["dict", "NestedDict"]:
# When dict comes from the frontend it comes as a
# list of dicts, so we need to convert it to a dict
# before passing it to the build method
_value = value.get("value")
if isinstance(_value, list):
params[key] = {
k: v
for item in value.get("value", [])
for k, v in item.items()
}
elif isinstance(_value, dict):
params[key] = _value
else: else:
params[key] = value.get("value") params[key] = value.get("value")

View file

@ -45,8 +45,12 @@ def get_memory_key(langchain_object):
"chat_history": "history", "chat_history": "history",
"history": "chat_history", "history": "chat_history",
} }
# Check if memory_key attribute exists
if hasattr(langchain_object.memory, "memory_key"):
memory_key = langchain_object.memory.memory_key memory_key = langchain_object.memory.memory_key
return mem_key_dict.get(memory_key) return mem_key_dict.get(memory_key)
else:
return None # or some other default value or action
def update_memory_keys(langchain_object, possible_new_mem_key): def update_memory_keys(langchain_object, possible_new_mem_key):

View file

@ -1,5 +1,6 @@
import ast import ast
import inspect import inspect
import textwrap
from typing import Dict, Union from typing import Dict, Union
from langchain.agents.tools import Tool from langchain.agents.tools import Tool
@ -7,7 +8,7 @@ from loguru import logger
def get_func_tool_params(func, **kwargs) -> Union[Dict, None]: def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
tree = ast.parse(inspect.getsource(func)) tree = ast.parse(textwrap.dedent(inspect.getsource(func)))
# Iterate over the statements in the abstract syntax tree # Iterate over the statements in the abstract syntax tree
for node in ast.walk(tree): for node in ast.walk(tree):
@ -58,13 +59,7 @@ def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]: def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]:
try: tree = ast.parse(textwrap.dedent(inspect.getsource(cls)))
tree = ast.parse(inspect.getsource(cls))
except IndentationError:
logger.error(
f"Error parsing class {cls.__name__}. Make sure there are no tabs in the code."
)
return None
tool_params = {} tool_params = {}

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.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES 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.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator from langflow.interface.embeddings.base import embedding_creator
from langflow.interface.importing.utils import get_function_custom 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): 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 return "prompt" if field_type == "Prompt" else field_type
@ -100,6 +103,7 @@ def add_new_custom_field(
# if it is, update the value # if it is, update the value
display_name = field_config.pop("display_name", field_name) display_name = field_config.pop("display_name", field_name)
field_type = field_config.pop("field_type", field_type) field_type = field_config.pop("field_type", field_type)
field_contains_list = "list" in field_type.lower()
field_type = process_type(field_type) field_type = process_type(field_type)
field_value = field_config.pop("value", field_value) field_value = field_config.pop("value", field_value)
field_advanced = field_config.pop("advanced", False) 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 a list, then it's a dropdown
# If options is None, then it's a list of strings # If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list) 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: if "name" in field_config:
warnings.warn( warnings.warn(
@ -172,7 +178,7 @@ def extract_type_from_optional(field_type):
Returns: Returns:
str: The extracted type, or an empty string if no type was found. 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 return match[1] if match else None
@ -284,6 +290,7 @@ def add_base_classes(frontend_node, return_types: List[str]):
def build_langchain_template_custom_component(custom_component: CustomComponent): def build_langchain_template_custom_component(custom_component: CustomComponent):
"""Build a custom component template for the langchain""" """Build a custom component template for the langchain"""
try:
logger.debug("Building custom component template") logger.debug("Building custom component template")
frontend_node = build_frontend_node(custom_component) frontend_node = build_frontend_node(custom_component)
@ -309,6 +316,16 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
) )
logger.debug("Added base classes") logger.debug("Added base classes")
return frontend_node return frontend_node
except Exception as exc:
raise HTTPException(
status_code=400,
detail={
"error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(),
},
) from exc
def load_files_from_path(path: str): def load_files_from_path(path: str):

View file

@ -77,11 +77,16 @@ def set_langchain_cache(settings):
import langchain import langchain
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
langchain_cache_type = os.getenv("LANGFLOW_LANGCHAIN_CACHE") if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
try:
cache_class = import_class( cache_class = import_class(
f"langchain.cache.{langchain_cache_type or settings.LANGCHAIN_CACHE}" f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}"
) )
logger.debug(f"Setting up LLM caching with {cache_class.__name__}") logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
langchain.llm_cache = cache_class() langchain.llm_cache = cache_class()
logger.info(f"LLM caching setup with {cache_class.__name__}") logger.info(f"LLM caching setup with {cache_class.__name__}")
except ImportError:
logger.warning(f"Could not import {cache_type}. ")
else:
logger.info("No LLM cache set.")

View file

@ -11,6 +11,7 @@ from langflow.api import router
from langflow.interface.utils import setup_llm_caching from langflow.interface.utils import setup_llm_caching
from langflow.services.database.utils import initialize_database from langflow.services.database.utils import initialize_database
from langflow.services.manager import initialize_services, teardown_services from langflow.services.manager import initialize_services, teardown_services
from langflow.services.plugins.langfuse import LangfuseInstance
from langflow.utils.logger import configure from langflow.utils.logger import configure
@ -41,6 +42,8 @@ def create_app():
app.on_event("startup")(initialize_database) app.on_event("startup")(initialize_database)
app.on_event("startup")(setup_llm_caching) app.on_event("startup")(setup_llm_caching)
app.on_event("shutdown")(teardown_services) app.on_event("shutdown")(teardown_services)
app.on_event("startup")(LangfuseInstance.update)
app.on_event("shutdown")(LangfuseInstance.teardown)
return app 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 ( from langflow.api.v1.callback import (
AsyncStreamingLLMCallbackHandler, AsyncStreamingLLMCallbackHandler,
StreamingLLMCallbackHandler, StreamingLLMCallbackHandler,
@ -6,6 +6,52 @@ from langflow.api.v1.callback import (
from langflow.processing.process import fix_memory_inputs, format_actions from langflow.processing.process import fix_memory_inputs, format_actions
from loguru import logger from loguru import logger
from langchain.agents.agent import AgentExecutor 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): 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}") logger.error(f"Error fixing memory inputs: {exc}")
try: try:
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)] trace_id = kwargs.pop("session_id", None)
output = await langchain_object.acall(inputs, callbacks=async_callbacks) callbacks = setup_callbacks(sync=False, trace_id=trace_id, **kwargs)
output = await langchain_object.acall(inputs, callbacks=callbacks)
except Exception as exc: except Exception as exc:
# make the error message more informative # make the error message more informative
logger.debug(f"Error: {str(exc)}") logger.debug(f"Error: {str(exc)}")
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)] trace_id = kwargs.pop("session_id", None)
output = langchain_object(inputs, callbacks=sync_callbacks) 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 = ( intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else [] output.get("intermediate_steps", []) if isinstance(output, dict) else []

View file

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

View file

@ -1,4 +1,5 @@
from collections import defaultdict from collections import defaultdict
import uuid
from fastapi import WebSocket, status from fastapi import WebSocket, status
from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse
from langflow.interface.utils import pil_to_base64 from langflow.interface.utils import pil_to_base64
@ -47,6 +48,7 @@ class ChatService(Service):
def __init__(self): def __init__(self):
self.active_connections: Dict[str, WebSocket] = {} self.active_connections: Dict[str, WebSocket] = {}
self.connection_ids: Dict[str, str] = {}
self.chat_history = ChatHistory() self.chat_history = ChatHistory()
self.chat_cache = cache_service self.chat_cache = cache_service
self.chat_cache.attach(self.update) self.chat_cache.attach(self.update)
@ -91,9 +93,13 @@ class ChatService(Service):
async def connect(self, client_id: str, websocket: WebSocket): async def connect(self, client_id: str, websocket: WebSocket):
self.active_connections[client_id] = 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): def disconnect(self, client_id: str):
self.active_connections.pop(client_id, None) self.active_connections.pop(client_id, None)
self.connection_ids.pop(client_id, None)
async def send_message(self, client_id: str, message: str): async def send_message(self, client_id: str, message: str):
websocket = self.active_connections[client_id] websocket = self.active_connections[client_id]
@ -135,6 +141,7 @@ class ChatService(Service):
langchain_object=langchain_object, langchain_object=langchain_object,
chat_inputs=chat_inputs, chat_inputs=chat_inputs,
websocket=self.active_connections[client_id], websocket=self.active_connections[client_id],
session_id=self.connection_ids[client_id],
) )
self.set_cache(client_id, langchain_object) self.set_cache(client_id, langchain_object)
except Exception as e: except Exception as e:

View file

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

View file

@ -1,12 +1,12 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Union from typing import Union
from uuid import UUID 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.database.models.user.user import User, UserUpdate
from langflow.services.utils import get_session from langflow.services.utils import get_session
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlmodel import Session from sqlmodel import Session
from typing import Optional
from sqlalchemy.orm.attributes import flag_modified 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( 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:
user_db = get_user_by_id(db, user_id)
if not user_db: if not user_db:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
user_db_by_username = get_user_by_username(db, user.username) # type: ignore # 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: # if user_db_by_username and user_db_by_username.id != user_id:
raise HTTPException(status_code=409, detail="Username already exists") # raise HTTPException(status_code=409, detail="Username already exists")
user_data = user.dict(exclude_unset=True) user_data = user.dict(exclude_unset=True)
changed = False
for attr, value in user_data.items(): for attr, value in user_data.items():
if hasattr(user_db, attr) and value is not None: if hasattr(user_db, attr) and value is not None:
setattr(user_db, attr, value) 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) user_db.updated_at = datetime.now(timezone.utc)
flag_modified(user_db, "updated_at") 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)): 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 user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
user = get_user_by_id(db, user_id)
return update_user(user_id, user_data, db) 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) id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
username: str = Field(index=True, unique=True) username: str = Field(index=True, unique=True)
password: str = Field() password: str = Field()
profile_image: Optional[str] = Field(default=None)
is_active: bool = Field(default=False) is_active: bool = Field(default=False)
is_superuser: bool = Field(default=False) is_superuser: bool = Field(default=False)
create_at: datetime = Field(default_factory=datetime.utcnow) create_at: datetime = Field(default_factory=datetime.utcnow)
@ -32,6 +33,7 @@ class UserCreate(SQLModel):
class UserRead(SQLModel): class UserRead(SQLModel):
id: UUID = Field(default_factory=uuid4) id: UUID = Field(default_factory=uuid4)
username: str = Field() username: str = Field()
profile_image: Optional[str] = Field()
is_active: bool = Field() is_active: bool = Field()
is_superuser: bool = Field() is_superuser: bool = Field()
create_at: datetime = Field() create_at: datetime = Field()
@ -40,7 +42,8 @@ class UserRead(SQLModel):
class UserUpdate(SQLModel): class UserUpdate(SQLModel):
username: Optional[str] = Field() profile_image: Optional[str] = Field()
password: Optional[str] = Field()
is_active: Optional[bool] = Field() is_active: Optional[bool] = Field()
is_superuser: Optional[bool] = Field() is_superuser: Optional[bool] = Field()
last_login_at: Optional[datetime] = Field() last_login_at: Optional[datetime] = Field()

View file

@ -0,0 +1,50 @@
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,
host=settings_manager.settings.LANGFUSE_HOST,
)
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

@ -30,7 +30,7 @@ class AuthSettings(BaseSettings):
# If AUTO_LOGIN = True # If AUTO_LOGIN = True
# > The application does not request login and logs in automatically as a super user. # > The application does not request login and logs in automatically as a super user.
AUTO_LOGIN: bool = False AUTO_LOGIN: bool = True
FIRST_SUPERUSER: str = "langflow" FIRST_SUPERUSER: str = "langflow"
FIRST_SUPERUSER_PASSWORD: str = "langflow" FIRST_SUPERUSER_PASSWORD: str = "langflow"

View file

@ -48,6 +48,10 @@ class Settings(BaseSettings):
REDIS_DB: int = 0 REDIS_DB: int = 0
REDIS_CACHE_EXPIRE: int = 3600 REDIS_CACHE_EXPIRE: int = 3600
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) @validator("CONFIG_DIR", pre=True, allow_reuse=True)
def set_langflow_dir(cls, value): def set_langflow_dir(cls, value):
if not value: if not value:

View file

@ -140,13 +140,16 @@ class FrontendNode(BaseModel):
@staticmethod @staticmethod
def handle_dict_type(field: TemplateField, _type: str) -> str: def handle_dict_type(field: TemplateField, _type: str) -> str:
"""Handles 'dict' type by replacing it with 'code' or 'file' based on the field name.""" """Handles 'dict' type by replacing it with 'code' or 'file' based on the field name."""
if "dict" in _type.lower(): if "dict" in _type.lower() and field.name == "dict_":
if field.name == "dict_":
field.field_type = "file" field.field_type = "file"
field.suffixes = [".json", ".yaml", ".yml"] field.suffixes = [".json", ".yaml", ".yml"]
field.file_types = ["json", "yaml", "yml"] field.file_types = ["json", "yaml", "yml"]
else: elif (
field.field_type = "code" _type.startswith("Dict")
or _type.startswith("Mapping")
or _type.startswith("dict")
):
field.field_type = "dict"
return _type return _type
@staticmethod @staticmethod
@ -240,20 +243,6 @@ class FrontendNode(BaseModel):
"description", "description",
} }
@staticmethod
def replace_dict_with_code_or_file(
field: TemplateField, _type: str, key: str
) -> str:
"""Replaces 'dict' type with 'code' or 'file'."""
if "dict" in _type.lower():
if key == "dict_":
field.field_type = "file"
field.suffixes = [".json", ".yaml", ".yml"]
field.file_types = ["json", "yaml", "yml"]
else:
field.field_type = "code"
return field.field_type
@staticmethod @staticmethod
def set_field_default_value(field: TemplateField, value: dict, key: str) -> None: def set_field_default_value(field: TemplateField, value: dict, key: str) -> None:
"""Sets the field value with the default value if present.""" """Sets the field value with the default value if present."""

View file

@ -170,11 +170,11 @@ class DocumentLoaderFrontNode(FrontendNode):
# add a metadata field of type dict # add a metadata field of type dict
self.template.add_field( self.template.add_field(
TemplateField( TemplateField(
field_type="code", field_type="dict",
required=True, required=True,
show=True, show=True,
name="metadata", name="metadata",
value="{}", value={},
display_name="Metadata", display_name="Metadata",
multiline=False, multiline=False,
) )

View file

@ -89,7 +89,7 @@ class EmbeddingFrontendNode(FrontendNode):
if field.name == "headers": if field.name == "headers":
field.show = False field.show = False
if field.name == "model_kwargs": if field.name == "model_kwargs":
field.field_type = "code" field.field_type = "dict"
field.advanced = True field.advanced = True
field.show = True field.show = True
elif field.name in [ elif field.name in [

View file

@ -153,10 +153,13 @@ class DictCodeFileFormatter(FieldFormatter):
key = field.name key = field.name
value = field.to_dict() value = field.to_dict()
_type = value["type"] _type = value["type"]
if "dict" in _type.lower(): if "dict" in _type.lower() and key == "dict_":
if key == "dict_":
field.field_type = "file" field.field_type = "file"
field.suffixes = [".json", ".yaml", ".yml"] field.suffixes = [".json", ".yaml", ".yml"]
field.file_types = ["json", "yaml", "yml"] field.file_types = ["json", "yaml", "yml"]
else: elif (
field.field_type = "code" _type.startswith("Dict")
or _type.startswith("Mapping")
or _type.startswith("dict")
):
field.field_type = "dict"

View file

@ -131,7 +131,7 @@ class LLMFrontendNode(FrontendNode):
if display_name := display_names_dict.get(field.name): if display_name := display_names_dict.get(field.name):
field.display_name = display_name field.display_name = display_name
if field.name == "model_kwargs": if field.name == "model_kwargs":
field.field_type = "code" field.field_type = "dict"
field.advanced = True field.advanced = True
field.show = True field.show = True
elif field.name in [ elif field.name in [

View file

@ -15,6 +15,7 @@ from langflow.template.template.base import Template
class PromptFrontendNode(FrontendNode): class PromptFrontendNode(FrontendNode):
@staticmethod @staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None: def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
# if field.field_type == "StringPromptTemplate" # if field.field_type == "StringPromptTemplate"
# change it to str # change it to str
PROMPT_FIELDS = [ PROMPT_FIELDS = [

View file

@ -21,5 +21,4 @@ class UtilitiesFrontendNode(FrontendNode):
field.field_type = "str" field.field_type = "str"
if isinstance(field.value, dict): if isinstance(field.value, dict):
field.field_type = "code"
field.value = orjson_dumps(field.value) field.value = orjson_dumps(field.value)

View file

@ -56,7 +56,7 @@ class VectorStoreFrontendNode(FrontendNode):
# Add search_kwargs field # Add search_kwargs field
extra_field = TemplateField( extra_field = TemplateField(
name="search_kwargs", name="search_kwargs",
field_type="code", field_type="NestedDict",
required=False, required=False,
placeholder="", placeholder="",
show=True, show=True,

View file

@ -48,5 +48,16 @@ def python_function(text: str) -> str:
return text return text
""" """
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"]
PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set] PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set]
DIRECT_TYPES = [
"str",
"bool",
"dict",
"int",
"float",
"Any",
"prompt",
"code",
"NestedDict",
]

View file

@ -2,12 +2,13 @@ import re
import inspect import inspect
import importlib import importlib
from functools import wraps 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 docstring_parser import parse
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants from langflow.utils import constants
from langchain.schema import Document
def build_template_from_function( def build_template_from_function(
@ -275,8 +276,6 @@ def format_dict(
value["password"] = is_password_field(key) value["password"] = is_password_field(key)
value["multiline"] = is_multiline_field(key) value["multiline"] = is_multiline_field(key)
replace_dict_type_with_code(value)
if key == "dict_": if key == "dict_":
set_dict_file_attributes(value) set_dict_file_attributes(value)
@ -406,14 +405,6 @@ def is_multiline_field(key: str) -> bool:
} }
def replace_dict_type_with_code(value: Dict[str, Any]) -> None:
"""
Replaces the type value with 'code' if the type is a dict.
"""
if "dict" in value["type"].lower():
value["type"] = "code"
def set_dict_file_attributes(value: Dict[str, Any]) -> None: def set_dict_file_attributes(value: Dict[str, Any]) -> None:
""" """
Sets the file attributes for the 'dict_' key. Sets the file attributes for the 'dict_' key.
@ -456,3 +447,12 @@ def add_options_to_field(
value["options"] = options_map[class_name] value["options"] = options_map[class_name]
value["list"] = True value["list"] = True
value["value"] = options_map[class_name][0] 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"

View file

@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View file

@ -21,3 +21,6 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -53,6 +53,7 @@
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-tooltip": "^5.21.1", "react-tooltip": "^5.21.1",
"react18-json-view": "^0.2.3",
"reactflow": "^11.8.3", "reactflow": "^11.8.3",
"rehype-mathjax": "^4.0.3", "rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
@ -95,6 +96,7 @@
}, },
"proxy": "http://127.0.0.1:7860", "proxy": "http://127.0.0.1:7860",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.38.0",
"@swc/cli": "^0.1.62", "@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80", "@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",

View file

@ -0,0 +1,77 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -6,6 +6,13 @@
text-align: center; text-align: center;
} }
.react-flow__node {
width: auto;
height: auto;
border-radius: auto;
min-width: inherit;
}
.App-logo { .App-logo {
height: 40vmin; height: 40vmin;
pointer-events: none; pointer-events: none;
@ -45,3 +52,36 @@
font-family: text-security-disc; font-family: text-security-disc;
src: url("assets/text-security-disc.woff") format("woff"); src: url("assets/text-security-disc.woff") format("woff");
} }
.json-view {
height: 370px !important;
background-color: #2c2c2c !important;
border-radius: 10px !important;
padding: 10px !important;
}
.jv-indent {
overflow-y: auto !important;
max-height: 310px !important;
border-radius: 10px;
}
.jv-indent::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
border-radius: 10px;
}
.jv-indent::-webkit-scrollbar-track {
background-color: #f1f1f1 !important;
border-radius: 10px;
}
.jv-indent::-webkit-scrollbar-thumb {
background-color: #ccc !important;
border-radius: 999px !important;
}
.jv-indent::-webkit-scrollbar-thumb:hover {
background-color: #bbb !important;
}

View file

@ -9,6 +9,7 @@ import React, {
import { Handle, Position, useUpdateNodeInternals } from "reactflow"; import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../../../components/codeAreaComponent"; import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
import Dropdown from "../../../../components/dropdownComponent"; import Dropdown from "../../../../components/dropdownComponent";
import FloatComponent from "../../../../components/floatComponent"; import FloatComponent from "../../../../components/floatComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
@ -16,15 +17,22 @@ import InputComponent from "../../../../components/inputComponent";
import InputFileComponent from "../../../../components/inputFileComponent"; import InputFileComponent from "../../../../components/inputFileComponent";
import InputListComponent from "../../../../components/inputListComponent"; import InputListComponent from "../../../../components/inputListComponent";
import IntComponent from "../../../../components/intComponent"; import IntComponent from "../../../../components/intComponent";
import KeypairListComponent from "../../../../components/keypairListComponent";
import PromptAreaComponent from "../../../../components/promptComponent"; import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent"; import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { TOOLTIP_EMPTY } from "../../../../constants/constants"; import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { TabsContext } from "../../../../contexts/tabsContext"; import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components"; import { ParameterComponentType } from "../../../../types/components";
import { TabsState } from "../../../../types/tabs"; import { TabsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils"; import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
} from "../../../../utils/reactflowUtils";
import { import {
nodeColors, nodeColors,
nodeIconsLucide, nodeIconsLucide,
@ -45,6 +53,7 @@ export default function ParameterComponent({
required = false, required = false,
optionalHandle = null, optionalHandle = null,
info = "", info = "",
showNode,
}: ParameterComponentType): JSX.Element { }: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null); const refHtml = useRef<HTMLDivElement & ReactNode>(null);
@ -67,20 +76,27 @@ export default function ParameterComponent({
updateNodeInternals(data.id); updateNodeInternals(data.id);
}, [data.id, position, updateNodeInternals]); }, [data.id, position, updateNodeInternals]);
const { reactFlowInstance } = useContext(typesContext); const groupedEdge = useRef(null);
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled = let disabled =
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ?? reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
false; false;
const { data: myData } = useContext(typesContext); const { data: myData } = useContext(typesContext);
const handleOnNewValue = (newValue: string | string[] | boolean): void => { const handleOnNewValue = (
newValue: string | string[] | boolean | Object[]
): void => {
let newData = cloneDeep(data); let newData = cloneDeep(data);
newData.node!.template[name].value = newValue; newData.node!.template[name].value = newValue;
setData(newData); setData(newData);
// Set state to pending // Set state to pending
//@ts-ignore //@ts-ignore
setTabsState((prev: TabsState) => { setTabsState((prev: TabsState) => {
if (!prev[tabId]) {
return prev;
}
return { return {
...prev, ...prev,
[tabId]: { [tabId]: {
@ -93,6 +109,8 @@ export default function ParameterComponent({
renderTooltips(); renderTooltips();
}; };
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => { useEffect(() => {
if (name === "openai_api_base") console.log(info); if (name === "openai_api_base") console.log(info);
// @ts-ignore // @ts-ignore
@ -108,20 +126,28 @@ export default function ParameterComponent({
}, [info]); }, [info]);
function renderTooltips() { function renderTooltips() {
let groupedObj = groupByFamily(myData, tooltipTitle!, left, flow!); let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, flow!);
groupedEdge.current = groupedObj;
if (groupedObj && groupedObj.length > 0) { if (groupedObj && groupedObj.length > 0) {
//@ts-ignore
//@ts-ignore //@ts-ignore
refHtml.current = groupedObj.map((item, index) => { refHtml.current = groupedObj.map((item, index) => {
const Icon: any = const Icon: any =
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"]; nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return ( return (
<>
{index === 0 && (
<span>
{left
? "Avaliable input components:"
: "Avaliable output components:"}
</span>
)}
<span <span
key={index} key={index}
className={classNames( className={classNames(
index > 0 ? "mt-2 flex items-center" : "flex items-center" index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
)} )}
> >
<div <div
@ -139,7 +165,7 @@ export default function ParameterComponent({
/> />
</div> </div>
<span className="ps-2 text-xs text-foreground"> <span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"} {nodeNames[item.family] ?? "Other"}{" "}
<span className="text-xs"> <span className="text-xs">
{" "} {" "}
{item.type === "" ? "" : " - "} {item.type === "" ? "" : " - "}
@ -157,6 +183,7 @@ export default function ParameterComponent({
</span> </span>
</span> </span>
</span> </span>
</>
); );
}); });
} else { } else {
@ -169,7 +196,43 @@ export default function ParameterComponent({
renderTooltips(); renderTooltips();
}, [tooltipTitle, flow]); }, [tooltipTitle, flow]);
return ( return !showNode ? (
left &&
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int") &&
!optionalHandle ? (
<></>
) : (
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
></Handle>
</ShadTooltip>
)
) : (
<div <div
ref={ref} ref={ref}
className="mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" className="mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2"
@ -205,10 +268,14 @@ export default function ParameterComponent({
type === "code" || type === "code" ||
type === "prompt" || type === "prompt" ||
type === "file" || type === "file" ||
type === "dict" ||
type === "NestedDict" ||
type === "int") && type === "int") &&
!optionalHandle ? ( !optionalHandle ? (
<></> <></>
) : ( ) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip <ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"} styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0} delayDuration={0}
@ -230,8 +297,13 @@ export default function ParameterComponent({
borderColor: color, borderColor: color,
top: position, top: position,
}} }}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle> ></Handle>
</ShadTooltip> </ShadTooltip>
</div>
</Button>
)} )}
{left === true && {left === true &&
@ -334,11 +406,52 @@ export default function ParameterComponent({
field_name={name} field_name={name}
setNodeClass={(nodeClass) => { setNodeClass={(nodeClass) => {
data.node = nodeClass; data.node = nodeClass;
const clone = cloneDeep(data);
clone.node = nodeClass;
setData(clone);
}} }}
nodeClass={data.node} nodeClass={data.node}
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={(e) => {
handleOnNewValue(e);
}}
/>
</div>
) : left === true && type === "NestedDict" ? (
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value ?? {
yourkey: "value",
}
}
onChange={(newValue) => {
data.node!.template[name].value = newValue;
handleOnNewValue(newValue);
}}
/>
</div>
) : left === true && type === "dict" ? (
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
data.node!.template[name].value = valueToNumbers;
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
handleOnNewValue(valueToNumbers);
}}
/> />
</div> </div>
) : ( ) : (

View file

@ -25,10 +25,48 @@ export default function GenericNode({
const [data, setData] = useState(olddata); const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(TabsContext); const { updateFlow, flows, tabId } = useContext(TabsContext);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance } = useContext(typesContext); const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
const [handles, setHandles] = useState<boolean[] | []>([]);
let numberOfInputs: boolean[] = [];
function countHandles(): void {
numberOfInputs = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map((templateCamp) => {
const { template } = data.node!;
if (template[templateCamp].input_types) return true;
if (!template[templateCamp].show) return false;
switch (template[templateCamp].type) {
case "str":
return false;
case "bool":
return false;
case "float":
return false;
case "code":
return false;
case "prompt":
return false;
case "file":
return false;
case "int":
return false;
default:
return true;
}
});
setHandles(numberOfInputs);
}
useEffect(() => {
countHandles();
}, []);
// State for outline color // State for outline color
const { sseData, isBuilding } = useSSE(); const { sseData, isBuilding } = useSSE();
useEffect(() => { useEffect(() => {
@ -50,8 +88,15 @@ export default function GenericNode({
}); });
updateFlow(flow); updateFlow(flow);
} }
countHandles();
}, [data]); }, [data]);
useEffect(() => {
setTimeout(() => {
updateNodeInternals(data.id);
}, 300);
}, [showNode]);
// New useEffect to watch for changes in sseData and update validation status // New useEffect to watch for changes in sseData and update validation status
useEffect(() => { useEffect(() => {
const relevantData = sseData[data.id]; const relevantData = sseData[data.id];
@ -70,27 +115,51 @@ export default function GenericNode({
data={data} data={data}
setData={setData} setData={setData}
deleteNode={deleteNode} deleteNode={deleteNode}
setShowNode={setShowNode}
numberOfHandles={handles}
showNode={showNode}
></NodeToolbarComponent> ></NodeToolbarComponent>
</NodeToolbar> </NodeToolbar>
<div <div
className={classNames( className={classNames(
selected ? "border border-ring" : "border", selected ? "border border-ring" : "border",
" transition-transform ",
showNode
? " w-96 scale-100 transform rounded-lg duration-500 ease-in-out "
: " transform-width w-26 h-26 scale-90 transform rounded-full duration-500 ",
"generic-node-div" "generic-node-div"
)} )}
> >
{data.node?.beta && ( {data.node?.beta && showNode && (
<div className="beta-badge-wrapper"> <div className="beta-badge-wrapper">
<div className="beta-badge-content">BETA</div> <div className="beta-badge-content">BETA</div>
</div> </div>
)} )}
<div className="generic-node-div-title"> <div>
<div className="generic-node-title-arrangement"> <div
className={
"generic-node-div-title " +
(!showNode
? " relative h-24 w-24 rounded-full "
: " justify-between rounded-t-lg ")
}
>
<div
className={
"generic-node-title-arrangement rounded-full" +
(!showNode && "justify-center")
}
>
<IconComponent <IconComponent
name={name} name={name}
className="generic-node-icon" className={
"generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12")
}
iconColor={`${nodeColors[types[data.type]]}`} iconColor={`${nodeColors[types[data.type]]}`}
/> />
{showNode && (
<div className="generic-node-tooltip-div"> <div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}> <ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary"> <div className="generic-node-tooltip-div text-primary">
@ -98,7 +167,104 @@ export default function GenericNode({
</div> </div>
</ShadTooltip> </ShadTooltip>
</div> </div>
)}
</div> </div>
<div>
{!showNode && (
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map(
(templateField: string, idx) =>
data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && (
<ParameterComponent
key={
(data.node!.template[
templateField
].input_types?.join(";") ??
data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[
templateField
].input_types?.join("\n") ??
data.node?.template[templateField].type
}
required={
data.node?.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
showNode={showNode}
/>
)
)}
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types &&
data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
/>
</>
)}
</div>
{showNode && (
<div className="round-button-div"> <div className="round-button-div">
<div> <div>
<Tooltip <Tooltip
@ -156,10 +322,24 @@ export default function GenericNode({
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
)}
</div>
</div> </div>
<div className="generic-node-desc"> {showNode && (
<div className="generic-node-desc-text">{data.node?.description}</div> <div
className={
showNode
? "generic-node-desc " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
: ""
}
>
{data.node?.description !== "" && showNode && (
<div className="generic-node-desc-text">
{data.node?.description}
</div>
)}
<> <>
{Object.keys(data.node!.template) {Object.keys(data.node!.template)
@ -184,14 +364,18 @@ export default function GenericNode({
nodeColors[ nodeColors[
types[data.node?.template[templateField].type!] types[data.node?.template[templateField].type!]
] ?? ] ??
nodeColors[data.node?.template[templateField].type!] ?? nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors.unknown nodeColors.unknown
} }
title={ title={
data.node?.template[templateField].display_name data.node?.template[templateField].display_name
? data.node.template[templateField].display_name ? data.node.template[templateField].display_name
: data.node?.template[templateField].name : data.node?.template[templateField].name
? toTitleCase(data.node.template[templateField].name) ? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField) : toTitleCase(templateField)
} }
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
@ -216,6 +400,7 @@ export default function GenericNode({
optionalHandle={ optionalHandle={
data.node?.template[templateField].input_types data.node?.template[templateField].input_types
} }
showNode={showNode}
/> />
) : ( ) : (
<></> <></>
@ -244,9 +429,11 @@ export default function GenericNode({
id={[data.type, data.id, ...data.node!.base_classes].join("|")} id={[data.type, data.id, ...data.node!.base_classes].join("|")}
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode}
/> />
</> </>
</div> </div>
)}
</div> </div>
</> </>
); );

View file

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

View file

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

View file

@ -0,0 +1,53 @@
import { useEffect } from "react";
import { DictComponentType } from "../../types/components";
import DictAreaModal from "../../modals/dictAreaModal";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
export default function DictComponent({
value,
onChange,
disabled,
editNode = false,
}: DictComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange([""]);
}
}, [disabled]);
useEffect(() => {
if (value) onChange(value);
}, [value]);
return (
<div
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
)}
>
{
<div className="flex w-full gap-3">
<DictAreaModal
value={value}
onChange={(obj) => {
onChange(obj);
}}
>
<Input
type="text"
className={
editNode
? "input-edit-node input-disable pointer-events-none cursor-pointer"
: "input-disable pointer-events-none cursor-pointer"
}
placeholder="Click to edit your dictionary..."
/>
</DictAreaModal>
</div>
}
</div>
);
}

View file

@ -10,8 +10,8 @@ export default function FloatComponent({
editNode = false, editNode = false,
}: FloatComponentType): JSX.Element { }: FloatComponentType): JSX.Element {
const step = 0.1; const step = 0.1;
const min = 0; const min = -2;
const max = 1; const max = 2;
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {
@ -27,10 +27,10 @@ export default function FloatComponent({
step={step} step={step}
min={min} min={min}
onInput={(event: React.ChangeEvent<HTMLInputElement>) => { onInput={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value < min.toString()) { if (Number(event.target.value) < min) {
event.target.value = min.toString(); event.target.value = min.toString();
} }
if (event.target.value > max.toString()) { if (Number(event.target.value) > max) {
event.target.value = max.toString(); event.target.value = max.toString();
} }
}} }}
@ -39,7 +39,7 @@ export default function FloatComponent({
disabled={disabled} disabled={disabled}
className={editNode ? "input-edit-node" : ""} className={editNode ? "input-edit-node" : ""}
placeholder={ placeholder={
editNode ? "Number 0 to 1" : "Type a number from zero to one" editNode ? "Number -2 to 2" : "Type a number from minus two to two"
} }
onChange={(event) => { onChange={(event) => {
onChange(event.target.value); onChange(event.target.value);

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

View file

@ -18,6 +18,11 @@ export default function InputListComponent({
} }
}, [disabled]); }, [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.
if (typeof value === "string") {
value = [value];
}
return ( return (
<div <div
className={classNames( className={classNames(

View file

@ -44,7 +44,7 @@ export default function IntComponent({
step="1" step="1"
min={min} min={min}
onInput={(event: React.ChangeEvent<HTMLInputElement>) => { onInput={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value < min.toString()) { if (Number(event.target.value) < min) {
event.target.value = min.toString(); event.target.value = min.toString();
} }
}} }}

View file

@ -0,0 +1,112 @@
import { useEffect } from "react";
import { KeyPairListComponentType } from "../../types/components";
import _ from "lodash";
import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
import { Input } from "../ui/input";
export default function KeypairListComponent({
value,
onChange,
disabled,
editNode = false,
duplicateKey,
}: KeyPairListComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange([""]);
}
}, [disabled]);
const handleChangeKey = (event, idx) => {
const newInputList = _.cloneDeep(value);
const oldKey = Object.keys(newInputList[idx])[0];
const updatedObj = { [event.target.value]: newInputList[idx][oldKey] };
newInputList[idx] = updatedObj;
onChange(newInputList);
};
const handleChangeValue = (newValue, idx) => {
const newInputList = _.cloneDeep(value);
const key = Object.keys(newInputList[idx])[0];
newInputList[idx][key] = newValue;
onChange(newInputList);
};
useEffect(() => {
if (value) onChange(value);
}, [value]);
return (
<div
className={classNames(
value?.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
)}
>
{value?.map((obj, index) => {
return Object.keys(obj).map((key, idx) => {
return (
<div key={idx} className="flex w-full gap-3">
<Input
type="text"
value={key.trim()}
className={classNames(
editNode ? "input-edit-node" : "",
duplicateKey ? "input-invalid" : ""
)}
placeholder="Type key..."
onChange={(event) => handleChangeKey(event, index)}
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Backspace") {
e.preventDefault();
e.stopPropagation();
}
}}
/>
<Input
type="text"
value={obj[key]}
className={editNode ? "input-edit-node" : ""}
placeholder="Type a value..."
onChange={(event) =>
handleChangeValue(event.target.value, index)
}
/>
{index === value.length - 1 ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(value);
newInputList.push({ "": "" });
onChange(newInputList);
}}
>
<IconComponent
name="Plus"
className={"h-4 w-4 hover:text-accent-foreground"}
/>
</button>
) : (
<button
onClick={() => {
let newInputList = _.cloneDeep(value);
newInputList.splice(index, 1);
onChange(newInputList);
}}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
/>
</button>
)}
</div>
);
});
})}
</div>
);
}

View file

@ -4,10 +4,10 @@ export default function LoadingComponent({
remSize, remSize,
}: LoadingComponentProps): JSX.Element { }: LoadingComponentProps): JSX.Element {
return ( return (
<div role="status" className="m-auto w-min"> <div role="status" className="flex flex-col items-center justify-center">
<svg <svg
aria-hidden="true" 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" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -3,7 +3,7 @@ import { useEffect } from "react";
import { TypeModal } from "../../constants/enums"; import { TypeModal } from "../../constants/enums";
import { postValidatePrompt } from "../../controllers/API"; import { postValidatePrompt } from "../../controllers/API";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components"; import { PromptAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
export default function PromptAreaComponent({ export default function PromptAreaComponent({
@ -14,7 +14,7 @@ export default function PromptAreaComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
}: TextAreaComponentType): JSX.Element { }: PromptAreaComponentType) {
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
onChange(""); onChange("");

View file

@ -1,7 +1,7 @@
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as React from "react"; import * as React from "react";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
const Dialog = DialogPrimitive.Root; const Dialog = DialogPrimitive.Root;
@ -13,7 +13,7 @@ const DialogPortal = ({
...props ...props
}: DialogPrimitive.DialogPortalProps) => ( }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}> <DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center"> <div className="nopan nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children} {children}
</div> </div>
</DialogPrimitive.Portal> </DialogPrimitive.Portal>
@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-blur-shared backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in", "nopan nodrag noundo nocopy 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 className
)} )}
{...props} {...props}
@ -44,14 +44,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"noundo nocopy fixed z-50 flex w-full flex-col gap-3 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0", "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 className
)} )}
{...props} {...props}
> >
{children} {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"> <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> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View file

@ -0,0 +1,108 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import * as React from "react";
import { cn } from "../../utils/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn("flex w-full items-center justify-between", className)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild></SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-3 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View file

@ -115,6 +115,9 @@ export const EDIT_DIALOG_SUBTITLE =
export const CODE_PROMPT_DIALOG_SUBTITLE = export const CODE_PROMPT_DIALOG_SUBTITLE =
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string."; "Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
export const CODE_DICT_DIALOG_SUBTITLE =
"Edit your dictionary. This dialog allows you to create your own customized dictionary. You can add as many key-value pairs as you want. While in edit mode, you can enter ({}) or ([]), and this will result in adding a new object or array.";
/** /**
* The base text for subtitle of Prompt Dialog * The base text for subtitle of Prompt Dialog
* @constant * @constant
@ -499,6 +502,21 @@ export const NOUNS: string[] = [
*/ */
export const USER_PROJECTS_HEADER = "My Collection"; 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. * URLs excluded from error retries.
* @constant * @constant
@ -519,6 +537,12 @@ export const CONTROL_INPUT_STATE = {
username: "", username: "",
}; };
export const CONTROL_PATCH_USER_STATE = {
password: "",
cnfPassword: "",
gradient: "",
};
export const CONTROL_LOGIN_STATE = { export const CONTROL_LOGIN_STATE = {
username: "", username: "",
password: "", password: "",

View file

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

View file

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

View file

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

View file

@ -26,6 +26,8 @@ const initialValue: typesContextType = {
setData: () => {}, setData: () => {},
setFetchError: () => {}, setFetchError: () => {},
fetchError: false, fetchError: false,
setFilterEdge: (filter) => {},
getFilterEdge: [],
}; };
export const typesContext = createContext<typesContextType>(initialValue); export const typesContext = createContext<typesContextType>(initialValue);
@ -39,6 +41,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
const [fetchError, setFetchError] = useState(false); const [fetchError, setFetchError] = useState(false);
const { setLoading } = useContext(alertContext); const { setLoading } = useContext(alertContext);
const { getAuthentication } = useContext(AuthContext); const { getAuthentication } = useContext(AuthContext);
const [getFilterEdge, setFilterEdge] = useState([]);
useEffect(() => { useEffect(() => {
// If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks. // If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
@ -113,6 +116,8 @@ export function TypesProvider({ children }: { children: ReactNode }) {
setData, setData,
fetchError, fetchError,
setFetchError, setFetchError,
setFilterEdge,
getFilterEdge,
}} }}
> >
{children} {children}

View file

@ -33,11 +33,16 @@ function ApiInterceptor() {
} }
const res = await renewAccessToken(refreshToken); const res = await renewAccessToken(refreshToken);
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token, res?.data?.refresh_token); login(res?.data?.access_token, res?.data?.refresh_token);
}
try { try {
if (error?.config?.headers) { if (error?.config?.headers) {
delete error.config.headers["Authorization"]; delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${accessToken}`; error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token"
)}`;
const response = await axios.request(error.config); const response = await axios.request(error.config);
return response; return response;
} }

View file

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

View file

@ -1,8 +1,10 @@
import { Infinity } from "lucide-react"; import { InfinityIcon } from "lucide-react";
import { forwardRef } from "react"; import { forwardRef } from "react";
const GradientSparkles = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>( export const GradientSparkles = forwardRef<
(props, ref) => { SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return ( return (
<> <>
<svg width="0" height="0" style={{ position: "absolute" }}> <svg width="0" height="0" style={{ position: "absolute" }}>
@ -13,10 +15,7 @@ const GradientSparkles = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
<Infinity stroke="url(#grad1)" ref={ref} {...props} /> <InfinityIcon stroke="url(#grad1)" ref={ref} {...props} />
</> </>
); );
} });
);
export default GradientSparkles;

View file

@ -14,6 +14,7 @@ import {
import CodeTabsComponent from "../../components/codeTabsComponent"; import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants"; import { EXPORT_CODE_DIALOG } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { TemplateVariableType } from "../../types/api"; import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components"; import { tweakType, uniqueTweakType } from "../../types/components";
@ -33,23 +34,27 @@ const ApiModal = forwardRef(
{ {
flow, flow,
children, children,
disable,
}: { }: {
flow: FlowType; flow: FlowType;
children: ReactNode; children: ReactNode;
disable: boolean;
}, },
ref ref
) => { ) => {
const { autoLogin } = useContext(AuthContext);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState("0"); const [activeTab, setActiveTab] = useState("0");
const tweak = useRef<tweakType>([]); const tweak = useRef<tweakType>([]);
const tweaksList = useRef<string[]>([]); const tweaksList = useRef<string[]>([]);
const { setTweak, getTweak, tabsState } = useContext(TabsContext); const { setTweak, getTweak, tabsState } = useContext(TabsContext);
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState); const pythonApiCode = getPythonApiCode(
const curl_code = getCurlCode(flow, tweak.current, tabsState); flow,
autoLogin,
tweak.current,
tabsState
);
const curl_code = getCurlCode(flow, autoLogin, tweak.current, tabsState);
const pythonCode = getPythonCode(flow, tweak.current, tabsState); const pythonCode = getPythonCode(flow, tweak.current, tabsState);
const widgetCode = getWidgetCode(flow, tabsState); const widgetCode = getWidgetCode(flow, autoLogin, tabsState);
const tweaksCode = buildTweaks(flow); const tweaksCode = buildTweaks(flow);
const codesArray = [ const codesArray = [
curl_code, curl_code,
@ -152,10 +157,15 @@ const ApiModal = forwardRef(
tweak.current.push(newTweak); tweak.current.push(newTweak);
} }
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState); const pythonApiCode = getPythonApiCode(
const curl_code = getCurlCode(flow, tweak.current, tabsState); flow,
autoLogin,
tweak.current,
tabsState
);
const curl_code = getCurlCode(flow, autoLogin, tweak.current, tabsState);
const pythonCode = getPythonCode(flow, tweak.current, tabsState); const pythonCode = getPythonCode(flow, tweak.current, tabsState);
const widgetCode = getWidgetCode(flow, tabsState); const widgetCode = getWidgetCode(flow, autoLogin, tabsState);
tabs![0].code = curl_code; tabs![0].code = curl_code;
tabs![1].code = pythonApiCode; tabs![1].code = pythonApiCode;
@ -201,8 +211,8 @@ const ApiModal = forwardRef(
} }
return ( return (
<BaseModal open={open} setOpen={setOpen} disable={disable}> <BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}> <BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">Code</span> <span className="pr-2">Code</span>
<IconComponent <IconComponent

View file

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

View file

@ -1,6 +1,7 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react"; import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
import CodeAreaComponent from "../../components/codeAreaComponent"; import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent"; import Dropdown from "../../components/dropdownComponent";
import FloatComponent from "../../components/floatComponent"; import FloatComponent from "../../components/floatComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
@ -8,6 +9,7 @@ import InputComponent from "../../components/inputComponent";
import InputFileComponent from "../../components/inputFileComponent"; import InputFileComponent from "../../components/inputFileComponent";
import InputListComponent from "../../components/inputListComponent"; import InputListComponent from "../../components/inputListComponent";
import IntComponent from "../../components/intComponent"; import IntComponent from "../../components/intComponent";
import KeypairListComponent from "../../components/keypairListComponent";
import PromptAreaComponent from "../../components/promptComponent"; import PromptAreaComponent from "../../components/promptComponent";
import TextAreaComponent from "../../components/textAreaComponent"; import TextAreaComponent from "../../components/textAreaComponent";
import ToggleShadComponent from "../../components/toggleShadComponent"; import ToggleShadComponent from "../../components/toggleShadComponent";
@ -26,6 +28,10 @@ import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs"; import { TabsState } from "../../types/tabs";
import {
convertObjToArray,
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
@ -36,50 +42,73 @@ const EditNodeModal = forwardRef(
setData, setData,
nodeLength, nodeLength,
children, children,
open,
onClose,
}: { }: {
data: NodeDataType; data: NodeDataType;
setData: (data: NodeDataType) => void; setData: (data: NodeDataType) => void;
nodeLength: number; nodeLength: number;
children: ReactNode; children: ReactNode;
open?: boolean;
onClose?: (close: boolean) => void;
}, },
ref ref
) => { ) => {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(open ?? false);
const [myData, setMyData] = useState(data); const [myData, setMyData] = useState(data);
const { setTabsState, tabId } = useContext(TabsContext); const { setTabsState, tabId } = useContext(TabsContext);
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
let disabled = let disabled =
reactFlowInstance reactFlowInstance
?.getEdges() ?.getEdges()
.some((edge) => edge.targetHandle === data.id) ?? false; .some((edge) => edge.targetHandle === data.id) ?? false;
function changeAdvanced(templateParam: string): void { function changeAdvanced(n) {
setMyData((old) => { let newData = cloneDeep(data);
let newData = cloneDeep(old); newData.node!.template[n].advanced = !newData.node!.template[n].advanced;
newData.node!.template[templateParam].advanced = setMyData(newData);
!newData.node!.template[templateParam].advanced;
return newData;
});
} }
const handleOnNewValue = ( const handleOnNewValue = (newValue: any, name) => {
newValue: string | string[] | boolean, let newData = cloneDeep(data);
name: string
) => {
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue; newData.node!.template[name].value = newValue;
return newData; setMyData(newData);
});
}; };
useEffect(() => { useEffect(() => {
setMyData(data); // reset data to what it is on node when opening modal setMyData(data); // reset data to what it is on node when opening modal
onClose!(modalOpen);
}, [modalOpen]); }, [modalOpen]);
const [obj, setObj] = useState({
arr: ["test", 123456, false, null],
boolean: false,
longString:
"long string long string long string long string long string long string",
number: 123456,
try: {
k1: 123,
k2: "123",
k3: false,
},
string: "string",
});
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const [dictArr, setDictArr] = useState([
{ yourKey: "yourValue" },
] as Object[]);
return ( return (
<BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}> <BaseModal
size="large-h-full"
open={modalOpen}
setOpen={setModalOpen}
onChangeOpenModal={(open) => {
let newData = cloneDeep(data);
setMyData(newData);
}}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={myData.node?.description!}> <BaseModal.Header description={myData.node?.description!}>
<span className="pr-2">{myData.type}</span> <span className="pr-2">{myData.type}</span>
@ -166,6 +195,52 @@ const EditNodeModal = forwardRef(
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
/> />
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={obj}
onChange={(newValue) => {
setObj(newValue);
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
myData.node.template[templateParam]
.value?.length === 0 ||
!myData.node.template[templateParam]
.value
? dictArr
: convertObjToArray(
myData.node.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
setErrorDuplicateKey(
hasDuplicateKeys(newValue)
);
if (hasDuplicateKeys(newValue)) {
setDictArr(newValue);
} else {
setDictArr(newValue);
myData.node!.template[
templateParam
].value = newValue;
}
}}
/>
</div>
) : myData.node.template[templateParam] ) : myData.node.template[templateParam]
.multiline ? ( .multiline ? (
<TextAreaComponent <TextAreaComponent

View file

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

View file

@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { ReactNode, useEffect } from "react";
import React from "react"; import React from "react";
import { import {
@ -14,13 +14,25 @@ import { modalHeaderType } from "../../types/components";
type ContentProps = { children: ReactNode }; type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string }; type HeaderProps = { children: ReactNode; description: string };
type FooterProps = { children: ReactNode }; type FooterProps = { children: ReactNode };
type TriggerProps = { children: ReactNode }; type TriggerProps = {
children: ReactNode;
asChild?: boolean;
disable?: boolean;
};
const Content: React.FC<ContentProps> = ({ children }) => { const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="h-full w-full">{children}</div>; return <div className="h-full w-full">{children}</div>;
}; };
const Trigger: React.FC<ContentProps> = ({ children }) => { const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
return <>{children}</>; return (
<DialogTrigger
className={asChild ? "" : "w-full"}
hidden={children ? false : true}
asChild={asChild}
>
{children}
</DialogTrigger>
);
}; };
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({ const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
@ -47,7 +59,6 @@ interface BaseModalProps {
]; ];
open?: boolean; open?: boolean;
setOpen?: (open: boolean) => void; setOpen?: (open: boolean) => void;
disable?: boolean;
size?: size?:
| "x-small" | "x-small"
| "smaller" | "smaller"
@ -57,13 +68,16 @@ interface BaseModalProps {
| "large-h-full" | "large-h-full"
| "small-h-full" | "small-h-full"
| "medium-h-full"; | "medium-h-full";
disable?: boolean;
onChangeOpenModal?: (open: boolean) => void;
} }
function BaseModal({ function BaseModal({
open, open,
setOpen, setOpen,
disable = false,
children, children,
size = "large", size = "large",
onChangeOpenModal,
}: BaseModalProps) { }: BaseModalProps) {
const headerChild = React.Children.toArray(children).find( const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header (child) => (child as React.ReactElement).type === Header
@ -117,20 +131,21 @@ function BaseModal({
break; break;
} }
useEffect(() => {
if (onChangeOpenModal) {
onChangeOpenModal(open);
}
}, [open]);
//UPDATE COLORS AND STYLE CLASSSES //UPDATE COLORS AND STYLE CLASSSES
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
className={"w-full " + (disable ? "button-disable" : "")}
hidden={triggerChild ? false : true}
>
{triggerChild} {triggerChild}
</DialogTrigger>
<DialogContent className={minWidth}> <DialogContent className={minWidth}>
<div className="truncate-doubleline word-break-break-word"> <div className="truncate-doubleline word-break-break-word">
{headerChild} {headerChild}
</div> </div>
<div className={`mt-2 flex flex-col ${height} w-full `}> <div className={`mt-2 flex flex-col ${height!} w-full `}>
{ContentChild} {ContentChild}
</div> </div>
{ContentFooter && ( {ContentFooter && (

View file

@ -28,8 +28,7 @@ export default function CodeAreaModal({
const { dark } = useContext(darkContext); const { dark } = useContext(darkContext);
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
const [height, setHeight] = useState<string | null>(null); const [height, setHeight] = useState<string | null>(null);
const { setErrorData, setSuccessData, isTweakPage } = const { setErrorData, setSuccessData } = useContext(alertContext);
useContext(alertContext);
const [error, setError] = useState<{ const [error, setError] = useState<{
detail: { error: string | undefined; traceback: string | undefined }; detail: { error: string | undefined; traceback: string | undefined };
} | null>(null); } | null>(null);
@ -165,18 +164,18 @@ export default function CodeAreaModal({
</div> </div>
<div <div
className={ className={
"w-full transition-all delay-500 " + "whitespace-break-spaces transition-all delay-500" +
(error?.detail.error !== undefined ? "h-2/6" : "h-0") (error?.detail?.error !== undefined ? "h-2/6" : "h-0")
} }
> >
<div className="mt-1 h-full w-full overflow-y-auto overflow-x-clip text-left custom-scroll"> <div className="mt-1 h-full max-h-[10rem] w-full overflow-y-auto overflow-x-clip text-left custom-scroll">
<h1 className="text-lg text-destructive"> <h1 className="text-lg text-destructive">
{error?.detail?.error} {error?.detail?.error}
</h1> </h1>
<div className="ml-2 w-full text-sm text-status-red word-break-break-word"> <div className="ml-2 w-full text-sm text-status-red word-break-break-word">
<pre className="w-full word-break-break-word"> <span className="w-full word-break-break-word">
{error?.detail?.traceback} {error?.detail?.traceback}
</pre> </span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,67 @@
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
// import "ace-builds/webpack-resolver";
import { useEffect, useState } from "react";
import JsonView from "react18-json-view";
import "react18-json-view/src/dark.css";
import "react18-json-view/src/style.css";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { CODE_DICT_DIALOG_SUBTITLE } from "../../constants/constants";
import BaseModal from "../baseModal";
export default function DictAreaModal({
children,
onChange,
value,
}): JSX.Element {
const [open, setOpen] = useState(false);
const [dictObj, setDictObj] = useState(value);
useEffect(() => {
if (value) setDictObj(value);
}, [value]);
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_DICT_DIALOG_SUBTITLE}>
<span className="pr-2">Edit Dictionary</span>
<IconComponent
name="BookMarked"
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex h-full w-full flex-col transition-all">
<JsonView
theme="vscode"
dark={true}
editable
enableClipboard
onEdit={(edit) => {
setDictObj(edit["src"]);
}}
src={dictObj}
/>
<div className="flex h-fit w-full justify-end">
<Button
className="mt-3"
type="submit"
onClick={() => {
onChange(dictObj);
setOpen(false);
}}
>
Save
</Button>
</div>
</div>
</BaseModal.Content>
</BaseModal>
);
}

View file

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

View file

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

View file

@ -122,15 +122,22 @@ export default function GenericModal({
function validatePrompt(closeModal: boolean): void { function validatePrompt(closeModal: boolean): void {
//nodeClass is always null on tweaks //nodeClass is always null on tweaks
postValidatePrompt(field_name, inputValue, nodeClass!) postValidatePrompt(field_name, inputValue, nodeClass!)
.then((apiReturn) => { .then((apiReturn) => {
if (apiReturn.data) { if (apiReturn.data) {
setValue(inputValue);
apiReturn.data.frontend_node["template"]["template"]["value"] =
inputValue;
setNodeClass!(apiReturn?.data?.frontend_node);
let inputVariables = apiReturn.data.input_variables ?? []; let inputVariables = apiReturn.data.input_variables ?? [];
if (inputVariables && inputVariables.length === 0) { if (inputVariables && inputVariables.length === 0) {
setIsEdit(true); setIsEdit(true);
setNoticeData({ setNoticeData({
title: "Your template does not have any variables.", title: "Your template does not have any variables.",
}); });
setModalOpen(false);
} else { } else {
setIsEdit(false); setIsEdit(false);
setSuccessData({ setSuccessData({
@ -163,7 +170,11 @@ export default function GenericModal({
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
return ( return (
<BaseModal open={modalOpen} setOpen={setModalOpen}> <BaseModal
onChangeOpenModal={(open) => {}}
open={modalOpen}
setOpen={setModalOpen}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header <BaseModal.Header
description={(() => { description={(() => {
@ -290,9 +301,7 @@ export default function GenericModal({
setModalOpen(false); setModalOpen(false);
break; break;
case TypeModal.PROMPT: case TypeModal.PROMPT:
!inputValue || inputValue === "" validatePrompt(false);
? setModalOpen(false)
: validatePrompt(false);
break; break;
default: default:

View file

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

View file

@ -1,10 +1,10 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { X } from "lucide-react";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent"; import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import LoadingComponent from "../../components/loadingComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
@ -16,8 +16,13 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import {
ADMIN_HEADER_DESCRIPTION,
ADMIN_HEADER_TITLE,
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { import {
addUser, addUser,
deleteUser, deleteUser,
@ -33,12 +38,19 @@ export default function AdminPage() {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [size, setPageSize] = useState(10); const [size, setPageSize] = useState(10);
const [index, setPageIndex] = useState(0); const [index, setPageIndex] = useState(1);
const [loadingUsers, setLoadingUsers] = useState(true); const [loadingUsers, setLoadingUsers] = useState(true);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const { userData } = useContext(AuthContext); const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0); const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const userList = useRef([]); const userList = useRef([]);
useEffect(() => { useEffect(() => {
@ -65,7 +77,9 @@ export default function AdminPage() {
function handleChangePagination(pageIndex: number, pageSize: number) { function handleChangePagination(pageIndex: number, pageSize: number) {
setLoadingUsers(true); setLoadingUsers(true);
getUsersPage(pageIndex, pageSize) setPageSize(pageSize);
setPageIndex(pageIndex);
getUsersPage(pageSize * (pageIndex - 1), pageSize)
.then((users) => { .then((users) => {
setTotalRowsCount(users["total_count"]); setTotalRowsCount(users["total_count"]);
userList.current = users["users"]; userList.current = users["users"];
@ -78,7 +92,7 @@ export default function AdminPage() {
} }
function resetFilter() { function resetFilter() {
setPageIndex(0); setPageIndex(1);
setPageSize(10); setPageSize(10);
getUsers(); getUsers();
} }
@ -168,70 +182,62 @@ export default function AdminPage() {
function handleNewUser(user: UserInputType) { function handleNewUser(user: UserInputType) {
addUser(user) addUser(user)
.then((res) => { .then((res) => {
updateUser(res["id"], {
is_active: user.is_active,
is_superuser: user.is_superuser,
}).then((res) => {
resetFilter(); resetFilter();
setSuccessData({ setSuccessData({
title: "Success! New user added!", title: "Success! New user added!",
}); });
});
}) })
.catch((error) => { .catch((error) => {
setErrorData({ setErrorData({
title: "Error on add new user", title: "Error when adding new user",
list: [error["response"]["data"]["detail"]], list: [error.response.data.detail],
}); });
}); });
} }
return ( return (
<> <>
<div className="flex flex-col">
<Header /> <Header />
{userData && ( {userData && (
<div className="main-page-panel"> <div className="admin-page-panel flex h-full flex-col pb-8">
<div className="m-auto flex h-full flex-row justify-center"> <div className="main-page-nav-arrangement">
<div className="basis-5/6"> <span className="main-page-nav-title">
<div className="m-auto flex h-full flex-col space-y-8 p-8 "> <IconComponent name="Shield" className="w-6" />
<div className="flex items-center justify-between space-y-2"> {ADMIN_HEADER_TITLE}
<div> </span>
<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>
<div className="flex items-center space-x-2"></div> <span className="admin-page-description-text">
</div> {ADMIN_HEADER_DESCRIPTION}
</span>
{userList.current.length === 0 && !loadingUsers && ( <div className="flex w-full justify-between px-4">
<> <div className="flex w-96 items-center gap-4">
<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 <Input
placeholder="Search Username"
value={inputValue} value={inputValue}
placeholder="Filter users..."
className="h-8 w-[150px] lg:w-[250px]"
onChange={(e) => handleFilterUsers(e.target.value)} onChange={(e) => handleFilterUsers(e.target.value)}
/> />
{inputValue.length > 0 && ( {inputValue.length > 0 ? (
<Button <div
className="cursor-pointer"
onClick={() => { onClick={() => {
setInputValue(""); setInputValue("");
setFilterUserList(userList.current); setFilterUserList(userList.current);
}} }}
variant="ghost"
className="h-8 px-2 lg:px-3"
> >
Reset <IconComponent name="X" className="w-6 text-foreground" />
<X className="ml-2 h-4 w-4" /> </div>
</Button> ) : (
<div>
<IconComponent
name="Search"
className="w-6 text-foreground"
/>
</div>
)} )}
</div> </div>
<div> <div>
@ -244,28 +250,34 @@ export default function AdminPage() {
onConfirm={(index, user) => { onConfirm={(index, user) => {
handleNewUser(user); handleNewUser(user);
}} }}
asChild
> >
<Button>New User</Button> <Button variant="primary">New User</Button>
</UserManagementModal> </UserManagementModal>
</div> </div>
</div> </div>
{loadingUsers && ( {loadingUsers ? (
<div> <div className="flex h-full w-full items-center justify-center">
<strong>Loading...</strong> <LoadingComponent remSize={12} />
</div> </div>
)} ) : userList.current.length === 0 ? (
<>
<div className="m-4 flex items-center justify-between text-sm">
No users registered.
</div>
</>
) : (
<>
<div <div
className={ className={
"max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" + "m-4 h-full overflow-x-hidden overflow-y-scroll rounded-md border-2 bg-background custom-scroll" +
(loadingUsers ? " border-0" : "") (loadingUsers ? " border-0" : "")
} }
> >
<Table className={"table-fixed bg-muted outline-1"}> <Table className={"table-fixed outline-1 "}>
<TableHeader <TableHeader
className={ className={
loadingUsers loadingUsers ? "hidden" : "table-fixed bg-muted outline-1"
? "hidden"
: "table-fixed bg-muted outline-1"
} }
> >
<TableRow> <TableRow>
@ -280,14 +292,11 @@ export default function AdminPage() {
</TableHeader> </TableHeader>
{!loadingUsers && ( {!loadingUsers && (
<TableBody> <TableBody>
{filterUserList.map( {filterUserList.map((user: UserInputType, index) => (
(user: UserInputType, index) => (
<TableRow key={index}> <TableRow key={index}>
<TableCell className="truncate py-2 font-medium"> <TableCell className="truncate py-2 font-medium">
<ShadTooltip content={user.id}> <ShadTooltip content={user.id}>
<span className="cursor-default"> <span className="cursor-default">{user.id}</span>
{user.id}
</span>
</ShadTooltip> </ShadTooltip>
</TableCell> </TableCell>
<TableCell className="truncate py-2"> <TableCell className="truncate py-2">
@ -297,8 +306,9 @@ export default function AdminPage() {
</span> </span>
</ShadTooltip> </ShadTooltip>
</TableCell> </TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left"> <TableCell className="relative left-1 truncate py-2 text-align-last-left">
<ConfirmationModal <ConfirmationModal
asChild
title="Edit" title="Edit"
titleHeader={`${user.username}`} titleHeader={`${user.username}`}
modalContentTitle="Attention!" modalContentTitle="Attention!"
@ -316,14 +326,17 @@ export default function AdminPage() {
); );
}} }}
> >
<div className="flex w-fit">
<Checkbox <Checkbox
id="is_active" id="is_active"
checked={user.is_active} checked={user.is_active}
/> />
</div>
</ConfirmationModal> </ConfirmationModal>
</TableCell> </TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left"> <TableCell className="relative left-1 truncate py-2 text-align-last-left">
<ConfirmationModal <ConfirmationModal
asChild
title="Edit" title="Edit"
titleHeader={`${user.username}`} titleHeader={`${user.username}`}
modalContentTitle="Attention!" modalContentTitle="Attention!"
@ -341,10 +354,12 @@ export default function AdminPage() {
); );
}} }}
> >
<div className="flex w-fit">
<Checkbox <Checkbox
id="is_superuser" id="is_superuser"
checked={user.is_superuser} checked={user.is_superuser}
/> />
</div>
</ConfirmationModal> </ConfirmationModal>
</TableCell> </TableCell>
<TableCell className="truncate py-2 "> <TableCell className="truncate py-2 ">
@ -397,10 +412,7 @@ export default function AdminPage() {
handleDeleteUser(user); handleDeleteUser(user);
}} }}
> >
<ShadTooltip <ShadTooltip content="Delete" side="top">
content="Delete"
side="top"
>
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer" className="ml-2 h-4 w-4 cursor-pointer"
@ -410,8 +422,7 @@ export default function AdminPage() {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) ))}
)}
</TableBody> </TableBody>
)} )}
</Table> </Table>
@ -421,17 +432,14 @@ export default function AdminPage() {
pageIndex={index} pageIndex={index}
pageSize={size} pageSize={size}
totalRowsCount={totalRowsCount} totalRowsCount={totalRowsCount}
paginate={(pageIndex, pageSize) => { paginate={(pageSize, pageIndex) => {
handleChangePagination(pageSize, pageIndex); handleChangePagination(pageIndex, pageSize);
}} }}
></PaginatorComponent> ></PaginatorComponent>
</> </>
</div>
</div>
</div>
</div>
)} )}
</div> </div>
)}
</> </>
); );
} }

View file

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

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