Merge branch 'logspace-ai:dev' into dev

This commit is contained in:
Robert Wilkins III 2023-05-14 16:04:41 -05:00 committed by GitHub
commit de5c4b71be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
144 changed files with 9981 additions and 17766 deletions

View file

@ -0,0 +1,15 @@
# LangFlow Demo Codespace Readme
These instructions will walk you through the process of running a LangFlow demo via GitHub Codespaces.
## Setup
### Create a Codespace in GitHub
To setup the demo, simply navigate to the Langflow repo, click the "+" button, and select "Create new Codespace". This will automatically create a new codespace in your browser, which you can use for the demo.
### Wait for everything to install
After the codespace is opened, you should see a new Terminal window in VS Code where langflow is installed. Once the install completes, `langflow` will launch the webserver and your application will be available via devcontainer port.
Note: VS Code should prompt you with a button to push once the port is available.

View file

@ -0,0 +1,32 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/universal
{
"name": "LangChain Demo Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.10",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {},
"ghcr.io/devcontainers/features/node": {}
},
"customizations": {
"vscode": {
"extensions": [
"actboy168.tasks",
"GitHub.copilot",
"ms-python.python",
"eamodio.gitlens"
]
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pipx install 'langflow>=0.0.33' && langflow --host 0.0.0.0"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View file

@ -1,11 +1,12 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/universal
{
"name": "Default Linux Universal",
"name": "LangChain Dev Container",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {}
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker": {}
},
"customizations": {
"vscode": {"extensions": [
@ -15,7 +16,7 @@
"sourcery.sourcery",
"eamodio.gitlens"
]}
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
@ -24,7 +25,7 @@
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "uname -a",
"postCreateCommand": "poetry install"
// Configure tool-specific properties.
// "customizations": {},

View file

@ -30,4 +30,4 @@ jobs:
run: poetry install
- name: Run unit tests
run: |
make test
make tests

View file

@ -1,4 +1,4 @@
.PHONY: all format lint build
.PHONY: all format lint build build_frontend install_frontend run_frontend run_backend dev help tests coverage
all: help
@ -8,7 +8,7 @@ coverage:
--cov-report xml \
--cov-report term-missing:skip-covered
test:
tests:
poetry run pytest tests
format:
@ -26,8 +26,15 @@ install_frontend:
run_frontend:
cd src/frontend && npm start
run_backend:
poetry run uvicorn langflow.main:app --port 5003 --reload
frontend:
make install_frontend
make run_frontend
install_backend:
poetry install
backend:
poetry run uvicorn langflow.main:app --port 7860 --reload --log-level debug
build_frontend:
cd src/frontend && CI='' npm run build
@ -64,3 +71,6 @@ help:
@echo 'build - build the frontend static files and package the project'
@echo 'publish - build the frontend static files and package the project and publish it to PyPI'
@echo 'dev - run the project in development mode with docker compose'
@echo 'tests - run the tests'
@echo 'coverage - run the tests and generate a coverage report'
@echo '----'

View file

@ -5,6 +5,7 @@
~ A User Interface For [LangChain](https://github.com/hwchase17/langchain) ~
<p>
<a href="https://huggingface.co/spaces/Logspace/LangFlow"><img src="https://huggingface.co/datasets/huggingface/badges/raw/main/open-in-hf-spaces-sm.svg" alt="HuggingFace Spaces"></a>
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/logspace-ai/langflow" />
<img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/logspace-ai/langflow" />
<img alt="" src="https://img.shields.io/github/repo-size/logspace-ai/langflow" />
@ -22,11 +23,28 @@ LangFlow is a GUI for [LangChain](https://github.com/hwchase17/langchain), desig
### <b>Locally</b>
You can install LangFlow from pip:
`pip install langflow`
```shell
pip install langflow
```
Next, run:
`langflow`
```shell
python -m langflow
```
or
```shell
langflow
```
### Deploy Langflow on Google Cloud Platform
Follow our step-by-step guide to deploy Langflow on Google Cloud Platform (GCP) using Google Cloud Shell. The guide is available in the [**Langflow in Google Cloud Platform**](GCP_DEPLOYMENT.md) document.
Alternatively, click the **"Open in Cloud Shell"** button below to launch Google Cloud Shell, clone the Langflow repository, and start an **interactive tutorial** that will guide you through the process of setting up the necessary resources and deploying Langflow on your GCP project.
[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/genome21/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md)
### Deploy Langflow on Google Cloud Platform

View file

@ -1,11 +0,0 @@
#! /bin/bash
cd src/frontend
docker build -t logspace/frontend_build -f build.Dockerfile .
cd ../backend
docker build -t logspace/backend_build -f build.Dockerfile .
cd ../../
VERSION=$(toml get --toml-path pyproject.toml tool.poetry.version)
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow:$VERSION .
docker push ibiscp/langflow:$VERSION

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "reactFlow",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

2061
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "langflow"
version = "0.0.56"
version = "0.0.69"
description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"]
maintainers = [
@ -29,7 +29,7 @@ google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0"
typer = "^0.7.0"
gunicorn = "^20.1.0"
langchain = "^0.0.131"
langchain = "^0.0.166"
openai = "^0.27.2"
types-pyyaml = "^6.0.12.8"
dill = "^0.3.6"
@ -45,7 +45,12 @@ lxml = "^4.9.2"
pysrt = "^1.1.2"
fake-useragent = "^1.1.3"
docstring-parser = "^0.15"
psycopg2 = "^2.9.6"
psycopg2-binary = "^2.9.6"
pyarrow = "^11.0.0"
websockets = "^11.0.2"
tiktoken = "^0.3.3"
wikipedia = "^1.4.0"
gptcache = "^0.1.23"
[tool.poetry.group.dev.dependencies]
black = "^23.1.0"
@ -56,6 +61,10 @@ httpx = "^0.23.3"
pytest = "^7.2.2"
types-requests = "^2.28.11"
requests = "^2.28.0"
pytest-cov = "^4.0.0"
pandas-stubs = "^2.0.0.230412"
types-pillow = "^9.5.0.2"
[tool.ruff]
line-length = 120

View file

@ -1,52 +0,0 @@
# `python-base` sets up all our shared environment variables
FROM python:3.10-slim
# python
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
\
# pip
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
\
# poetry
# https://python-poetry.org/docs/configuration/#using-environment-variables
POETRY_VERSION=1.4.0 \
# make poetry install to this location
POETRY_HOME="/opt/poetry" \
# make poetry create the virtual environment in the project's root
# it gets named `.venv`
POETRY_VIRTUALENVS_IN_PROJECT=true \
# do not ask any interactive question
POETRY_NO_INTERACTION=1 \
\
# paths
# this is where our requirements + virtual environment will live
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"
# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# deps for installing poetry
curl \
# deps for building python deps
build-essential libpq-dev
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
RUN curl -sSL https://install.python-poetry.org | python3 -
# copy project requirement files here to ensure they will be cached.
WORKDIR /app
COPY poetry.lock pyproject.toml ./
COPY langflow/ ./langflow
# poetry install
RUN poetry install --without dev
# build wheel
RUN poetry build -f wheel

View file

@ -1,6 +0,0 @@
#! /bin/bash
docker build -t logspace/backend_build -f build.Dockerfile .
VERSION=$(toml get --toml-path pyproject.toml tool.poetry.version)
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow:$VERSION .
docker push ibiscp/langflow:$VERSION

View file

@ -1 +1,4 @@
from langflow.interface.loading import load_flow_from_json # noqa
from langflow.cache import cache_manager
from langflow.interface.loading import load_flow_from_json
__all__ = ["load_flow_from_json", "cache_manager"]

View file

@ -3,6 +3,10 @@ from pydantic import BaseModel, validator
from langflow.graph.utils import extract_input_variables_from_prompt
class CacheResponse(BaseModel):
data: dict
class Code(BaseModel):
code: str

View file

@ -0,0 +1,32 @@
import asyncio
from typing import Any
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
from langflow.api.schemas import ChatResponse
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
"""Callback handler for streaming LLM responses."""
def __init__(self, websocket):
self.websocket = websocket
async def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
await self.websocket.send_json(resp.dict())
class StreamingLLMCallbackHandler(BaseCallbackHandler):
"""Callback handler for streaming LLM responses."""
def __init__(self, websocket):
self.websocket = websocket
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
loop = asyncio.get_event_loop()
coroutine = self.websocket.send_json(resp.dict())
asyncio.run_coroutine_threadsafe(coroutine, loop)

View file

@ -0,0 +1,26 @@
from fastapi import (
APIRouter,
WebSocket,
WebSocketDisconnect,
WebSocketException,
status,
)
from langflow.api.chat_manager import ChatManager
from langflow.utils.logger import logger
router = APIRouter()
chat_manager = ChatManager()
@router.websocket("/chat/{client_id}")
async def websocket_endpoint(client_id: str, websocket: WebSocket):
"""Websocket endpoint for chat."""
try:
await chat_manager.handle_websocket(client_id, websocket)
except WebSocketException as exc:
logger.error(exc)
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
except WebSocketDisconnect as exc:
logger.error(exc)
await websocket.close(code=status.WS_1000_NORMAL_CLOSURE, reason=str(exc))

View file

@ -0,0 +1,221 @@
import asyncio
import json
from collections import defaultdict
from typing import Dict, List
from fastapi import WebSocket, status
from langflow.api.schemas import ChatMessage, ChatResponse, FileResponse
from langflow.cache import cache_manager
from langflow.cache.manager import Subject
from langflow.interface.run import (
get_result_and_steps,
load_or_build_langchain_object,
)
from langflow.interface.utils import pil_to_base64, try_setting_streaming_options
from langflow.utils.logger import logger
class ChatHistory(Subject):
def __init__(self):
super().__init__()
self.history: Dict[str, List[ChatMessage]] = defaultdict(list)
def add_message(self, client_id: str, message: ChatMessage):
"""Add a message to the chat history."""
self.history[client_id].append(message)
if not isinstance(message, FileResponse):
self.notify()
def get_history(self, client_id: str, filter=True) -> List[ChatMessage]:
"""Get the chat history for a client."""
if history := self.history.get(client_id, []):
if filter:
return [msg for msg in history if msg.type not in ["start", "stream"]]
return history
else:
return []
def empty_history(self, client_id: str):
"""Empty the chat history for a client."""
self.history[client_id] = []
class ChatManager:
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {}
self.chat_history = ChatHistory()
self.cache_manager = cache_manager
self.cache_manager.attach(self.update)
def on_chat_history_update(self):
"""Send the last chat message to the client."""
client_id = self.cache_manager.current_client_id
if client_id in self.active_connections:
chat_response = self.chat_history.get_history(client_id, filter=False)[-1]
if chat_response.is_bot:
# Process FileResponse
if isinstance(chat_response, FileResponse):
# If data_type is pandas, convert to csv
if chat_response.data_type == "pandas":
chat_response.data = chat_response.data.to_csv()
elif chat_response.data_type == "image":
# Base64 encode the image
chat_response.data = pil_to_base64(chat_response.data)
# get event loop
loop = asyncio.get_event_loop()
coroutine = self.send_json(client_id, chat_response)
asyncio.run_coroutine_threadsafe(coroutine, loop)
def update(self):
if self.cache_manager.current_client_id in self.active_connections:
self.last_cached_object_dict = self.cache_manager.get_last()
# Add a new ChatResponse with the data
chat_response = FileResponse(
message=None,
type="file",
data=self.last_cached_object_dict["obj"],
data_type=self.last_cached_object_dict["type"],
)
self.chat_history.add_message(
self.cache_manager.current_client_id, chat_response
)
async def connect(self, client_id: str, websocket: WebSocket):
await websocket.accept()
self.active_connections[client_id] = websocket
def disconnect(self, client_id: str):
self.active_connections.pop(client_id, None)
async def send_message(self, client_id: str, message: str):
websocket = self.active_connections[client_id]
await websocket.send_text(message)
async def send_json(self, client_id: str, message: ChatMessage):
websocket = self.active_connections[client_id]
await websocket.send_json(message.dict())
async def process_message(self, client_id: str, payload: Dict):
# Process the graph data and chat message
chat_message = payload.pop("message", "")
chat_message = ChatMessage(message=chat_message)
self.chat_history.add_message(client_id, chat_message)
graph_data = payload
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
await self.send_json(client_id, start_resp)
is_first_message = len(self.chat_history.get_history(client_id=client_id)) == 0
# Generate result and thought
try:
logger.debug("Generating result and thought")
result, intermediate_steps = await process_graph(
graph_data=graph_data,
is_first_message=is_first_message,
chat_message=chat_message,
websocket=self.active_connections[client_id],
)
except Exception as e:
# Log stack trace
logger.exception(e)
self.chat_history.empty_history(client_id)
raise e
# Send a response back to the frontend, if needed
intermediate_steps = intermediate_steps or ""
history = self.chat_history.get_history(client_id, filter=False)
file_responses = []
if history:
# Iterate backwards through the history
for msg in reversed(history):
if isinstance(msg, FileResponse):
if msg.data_type == "image":
# Base64 encode the image
msg.data = pil_to_base64(msg.data)
file_responses.append(msg)
if msg.type == "start":
break
response = ChatResponse(
message=result,
intermediate_steps=intermediate_steps.strip(),
type="end",
files=file_responses,
)
await self.send_json(client_id, response)
self.chat_history.add_message(client_id, response)
async def handle_websocket(self, client_id: str, websocket: WebSocket):
await self.connect(client_id, websocket)
try:
chat_history = self.chat_history.get_history(client_id)
# iterate and make BaseModel into dict
chat_history = [chat.dict() for chat in chat_history]
await websocket.send_json(chat_history)
while True:
json_payload = await websocket.receive_json()
try:
payload = json.loads(json_payload)
except TypeError:
payload = json_payload
if "clear_history" in payload:
self.chat_history.history[client_id] = []
continue
with self.cache_manager.set_client_id(client_id):
await self.process_message(client_id, payload)
except Exception as e:
# Handle any exceptions that might occur
logger.exception(e)
# send a message to the client
await self.active_connections[client_id].close(
code=status.WS_1011_INTERNAL_ERROR, reason=str(e)[:120]
)
self.disconnect(client_id)
finally:
try:
connection = self.active_connections.get(client_id)
if connection:
await connection.close(code=1000, reason="Client disconnected")
self.disconnect(client_id)
except Exception as e:
logger.exception(e)
self.disconnect(client_id)
async def process_graph(
graph_data: Dict,
is_first_message: bool,
chat_message: ChatMessage,
websocket: WebSocket,
):
langchain_object = load_or_build_langchain_object(graph_data, is_first_message)
langchain_object = try_setting_streaming_options(langchain_object, websocket)
logger.debug("Loaded langchain object")
if langchain_object is None:
# Raise user facing error
raise ValueError(
"There was an error loading the langchain_object. Please, check all the nodes and try again."
)
# Generate result and thought
try:
logger.debug("Generating result and thought")
result, intermediate_steps = await get_result_and_steps(
langchain_object, chat_message.message or "", websocket=websocket
)
logger.debug("Generated result and intermediate_steps")
return result, intermediate_steps
except Exception as e:
# Log stack trace
logger.exception(e)
raise e

View file

@ -1,8 +1,13 @@
import logging
from typing import Any, Dict
from fastapi import APIRouter, HTTPException
from langflow.api.schemas import (
ExportedFlow,
GraphData,
PredictRequest,
PredictResponse,
)
from langflow.interface.run import process_graph_cached
from langflow.interface.types import build_langchain_types_dict
@ -16,10 +21,14 @@ def get_all():
return build_langchain_types_dict()
@router.post("/predict")
def get_load(data: Dict[str, Any]):
@router.post("/predict", response_model=PredictResponse)
async def get_load(predict_request: PredictRequest):
try:
return process_graph_cached(data)
exported_flow: ExportedFlow = predict_request.exported_flow
graph_data: GraphData = exported_flow.data
data = graph_data.dict()
response = process_graph_cached(data, predict_request.message)
return PredictResponse(result=response.get("result", ""))
except Exception as e:
# Log stack trace
logger.exception(e)

View file

@ -0,0 +1,70 @@
from typing import Any, Dict, List, Union
from pydantic import BaseModel, validator
class GraphData(BaseModel):
"""Data inside the exported flow."""
nodes: List[Dict[str, Any]]
edges: List[Dict[str, Any]]
class ExportedFlow(BaseModel):
"""Exported flow from LangFlow."""
description: str
name: str
id: str
data: GraphData
class PredictRequest(BaseModel):
"""Predict request schema."""
message: str
exported_flow: ExportedFlow
class PredictResponse(BaseModel):
"""Predict response schema."""
result: str
class ChatMessage(BaseModel):
"""Chat message schema."""
is_bot: bool = False
message: Union[str, None] = None
type: str = "human"
class ChatResponse(ChatMessage):
"""Chat response schema."""
intermediate_steps: str
type: str
is_bot: bool = True
files: list = []
@validator("type")
def validate_message_type(cls, v):
if v not in ["start", "stream", "end", "error", "info", "file"]:
raise ValueError("type must be start, stream, end, error, info, or file")
return v
class FileResponse(ChatMessage):
"""File response schema."""
data: Any
data_type: str
type: str = "file"
is_bot: bool = True
@validator("data_type")
def validate_data_type(cls, v):
if v not in ["image", "csv"]:
raise ValueError("data_type must be image or csv")
return v

View file

@ -1,3 +1,5 @@
import json
from fastapi import APIRouter, HTTPException
from langflow.api.base import (
@ -7,6 +9,8 @@ from langflow.api.base import (
PromptValidationResponse,
validate_prompt,
)
from langflow.graph.nodes import VectorStoreNode
from langflow.interface.run import build_graph
from langflow.utils.logger import logger
from langflow.utils.validate import validate_code
@ -33,3 +37,21 @@ def post_validate_prompt(prompt: Prompt):
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e
# validate node
@router.post("/node/{node_id}", status_code=200)
def post_validate_node(node_id: str, data: dict):
try:
# build graph
graph = build_graph(data)
# validate node
node = graph.get_node(node_id)
if node is None:
raise ValueError(f"Node {node_id} not found")
if not isinstance(node, VectorStoreNode):
node.build()
return json.dumps({"valid": True, "params": str(node._built_object_repr())})
except Exception as e:
logger.exception(e)
return json.dumps({"valid": False})

View file

@ -0,0 +1 @@
from langflow.cache.manager import cache_manager # noqa

View file

@ -7,9 +7,12 @@ import os
import tempfile
from collections import OrderedDict
from pathlib import Path
from typing import Any, Dict
import dill # type: ignore
CACHE: Dict[str, Any] = {}
def create_cache_folder(func):
def wrapper(*args, **kwargs):
@ -44,7 +47,8 @@ def memoize_dict(maxsize=128):
def clear_cache():
cache.clear()
wrapper.clear_cache = clear_cache
wrapper.clear_cache = clear_cache # type: ignore
wrapper.cache = cache # type: ignore
return wrapper
return decorator
@ -116,7 +120,8 @@ def save_binary_file(content: str, file_name: str, accepted_types: list[str]) ->
# Get the destination folder
cache_path = Path(tempfile.gettempdir()) / PREFIX
if content is None:
raise ValueError("Please, reload the file in the loader.")
data = content.split(",")[1]
decoded_bytes = base64.b64decode(data)

150
src/backend/langflow/cache/manager.py vendored Normal file
View file

@ -0,0 +1,150 @@
from contextlib import contextmanager
from typing import Any, Awaitable, Callable, List, Optional
import pandas as pd
from PIL import Image
class Subject:
"""Base class for implementing the observer pattern."""
def __init__(self):
self.observers: List[Callable[[], None]] = []
def attach(self, observer: Callable[[], None]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], None]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
observer()
class AsyncSubject:
"""Base class for implementing the async observer pattern."""
def __init__(self):
self.observers: List[Callable[[], Awaitable]] = []
def attach(self, observer: Callable[[], Awaitable]):
"""Attach an observer to the subject."""
self.observers.append(observer)
def detach(self, observer: Callable[[], Awaitable]):
"""Detach an observer from the subject."""
self.observers.remove(observer)
async def notify(self):
"""Notify all observers about an event."""
for observer in self.observers:
if observer is None:
continue
await observer()
class CacheManager(Subject):
"""Manages cache for different clients and notifies observers on changes."""
def __init__(self):
super().__init__()
self.CACHE = {}
self.current_client_id = None
self.current_cache = {}
@contextmanager
def set_client_id(self, client_id: str):
"""
Context manager to set the current client_id and associated cache.
Args:
client_id (str): The client identifier.
"""
previous_client_id = self.current_client_id
self.current_client_id = client_id
self.current_cache = self.CACHE.setdefault(client_id, {})
try:
yield
finally:
self.current_client_id = previous_client_id
self.current_cache = self.CACHE.get(self.current_client_id, {})
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
"""
Add an object to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The object to cache.
obj_type (str): The type of the object.
"""
object_extensions = {
"image": "png",
"pandas": "csv",
}
if obj_type in object_extensions:
_extension = object_extensions[obj_type]
else:
_extension = type(obj).__name__.lower()
self.current_cache[name] = {
"obj": obj,
"type": obj_type,
"extension": extension or _extension,
}
self.notify()
def add_pandas(self, name: str, obj: Any):
"""
Add a pandas DataFrame or Series to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The pandas DataFrame or Series object.
"""
if isinstance(obj, (pd.DataFrame, pd.Series)):
self.add(name, obj.to_csv(), "pandas", extension="csv")
else:
raise ValueError("Object is not a pandas DataFrame or Series")
def add_image(self, name: str, obj: Any, extension: str = "png"):
"""
Add a PIL Image to the current client's cache.
Args:
name (str): The cache key.
obj (Any): The PIL Image object.
"""
if isinstance(obj, Image.Image):
self.add(name, obj, "image", extension=extension)
else:
raise ValueError("Object is not a PIL Image")
def get(self, name: str):
"""
Get an object from the current client's cache.
Args:
name (str): The cache key.
Returns:
The cached object associated with the given cache key.
"""
return self.current_cache[name]
def get_last(self):
"""
Get the last added item in the current client's cache.
Returns:
The last added item in the cache.
"""
return list(self.current_cache.values())[-1]
cache_manager = CacheManager()

View file

@ -6,6 +6,7 @@ chains:
- SeriesCharacterChain
- MidJourneyPromptChain
- TimeTravelGuideChain
- SQLDatabaseChain
agents:
- ZeroShotAgent
@ -40,6 +41,28 @@ tools:
- Tool
- PythonFunction
- JsonSpec
- News API
- TMDB API
- Podcast API
- QuerySQLDataBaseTool
- InfoSQLDatabaseTool
- ListSQLDatabaseTool
# - QueryCheckerTool
- BingSearchRun
- GoogleSearchRun
- GoogleSearchResults
- GoogleSerperRun
- JsonListKeysTool
- JsonGetValueTool
- PythonREPLTool
- PythonAstREPLTool
- RequestsGetTool
- RequestsPostTool
- RequestsPatchTool
- RequestsPutTool
- RequestsDeleteTool
- WikipediaQueryRun
- WolframAlphaQueryRun
wrappers:
- RequestsWrapper
@ -91,4 +114,16 @@ documentloaders:
textsplitters:
- CharacterTextSplitter
utilities:
- BingSearchAPIWrapper
- GoogleSearchAPIWrapper
- GoogleSerperAPIWrapper
- SearxResults
- SearxSearchWrapper
- SerpAPIWrapper
- WikipediaAPIWrapper
- WolframAlphaAPIWrapper
# - ZapierNLAWrapper
- SQLDatabase
dev: false

View file

@ -12,6 +12,14 @@ CUSTOM_NODES = {
"VectorStoreRouterAgent": nodes.VectorStoreRouterAgentNode(),
"SQLAgent": nodes.SQLAgentNode(),
},
"utilities": {
"SQLDatabase": nodes.SQLDatabaseNode(),
},
"chains": {
"SeriesCharacterChain": nodes.SeriesCharacterChainNode(),
"TimeTravelGuideChain": nodes.TimeTravelGuideChainNode(),
"MidJourneyPromptChain": nodes.MidJourneyPromptChainNode(),
},
}

View file

@ -4,16 +4,18 @@
# - Build each inner agent first, then build the outer agent
import contextlib
import inspect
import types
import warnings
from copy import deepcopy
from typing import Any, Dict, List, Optional
from langflow.cache import utils as cache_utils
from langflow.cache import base as cache_utils
from langflow.graph.constants import DIRECT_TYPES
from langflow.interface import loading
from langflow.interface.listing import ALL_TYPES_DICT
from langflow.utils.logger import logger
from langflow.utils.util import sync_to_async
class Node:
@ -158,13 +160,21 @@ class Node:
continue
result = value.build()
# If the key is "func", then we need to use the run method
if key == "func" and not isinstance(result, types.FunctionType):
# func can be PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n')
# so we need to check if there is an attribute called run
if hasattr(result, "run"):
result = result.run # type: ignore
elif hasattr(result, "get_function"):
result = result.get_function() # type: ignore
if key == "func":
if not isinstance(result, types.FunctionType):
# func can be
# PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n')
# so we need to check if there is an attribute called run
if hasattr(result, "run"):
result = result.run # type: ignore
elif hasattr(result, "get_function"):
result = result.get_function() # type: ignore
elif inspect.iscoroutinefunction(result):
self.params["coroutine"] = result
else:
# turn result which is a function into a coroutine
# so that it can be awaited
self.params["coroutine"] = sync_to_async(result)
self.params[key] = result
elif isinstance(value, list) and all(
@ -202,7 +212,11 @@ class Node:
"VectorStoreRouterAgent",
"VectorStoreAgent",
"VectorStoreInfo",
] or self.node_type in ["VectorStoreInfo", "VectorStoreRouterToolkit"]:
] or self.node_type in [
"VectorStoreInfo",
"VectorStoreRouterToolkit",
"SQLDatabase",
]:
return self._built_object
return deepcopy(self._built_object)
@ -218,6 +232,9 @@ class Node:
def __hash__(self) -> int:
return id(self)
def _built_object_repr(self):
return repr(self._built_object)
class Edge:
def __init__(self, source: "Node", target: "Node"):

View file

@ -101,6 +101,10 @@ class ChainNode(Node):
self.params[key] = value.build(tools=tools, force=force)
self._build()
#! Cannot deepcopy SQLDatabaseChain
if self.node_type in ["SQLDatabaseChain"]:
return self._built_object
return deepcopy(self._built_object)
@ -135,6 +139,13 @@ class DocumentLoaderNode(Node):
def __init__(self, data: Dict):
super().__init__(data, base_type="documentloaders")
def _built_object_repr(self):
# This built_object is a list of documents. Maybe we should
# show how many documents are in the list?
if self._built_object:
return f"""{self.node_type}({len(self._built_object)} documents)\nDocuments: {self._built_object[:3]}..."""
return f"{self.node_type}()"
class EmbeddingNode(Node):
def __init__(self, data: Dict):
@ -145,6 +156,9 @@ class VectorStoreNode(Node):
def __init__(self, data: Dict):
super().__init__(data, base_type="vectorstores")
def _built_object_repr(self):
return "Vector stores can take time to build. It will build on the first query."
class MemoryNode(Node):
def __init__(self, data: Dict):
@ -154,3 +168,10 @@ class MemoryNode(Node):
class TextSplitterNode(Node):
def __init__(self, data: Dict):
super().__init__(data, base_type="textsplitters")
def _built_object_repr(self):
# This built_object is a list of documents. Maybe we should
# show how many documents are in the list?
if self._built_object:
return f"""{self.node_type}({len(self._built_object)} documents)\nDocuments: {self._built_object[:3]}..."""
return f"{self.node_type}()"

View file

@ -1,3 +1,4 @@
from abc import ABC
from typing import Any, List, Optional
from langchain import LLMChain
@ -26,15 +27,32 @@ from langchain.agents.agent_toolkits.vectorstore.prompt import (
)
from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS
from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS as SQL_FORMAT_INSTRUCTIONS
from langchain.llms.base import BaseLLM
from langchain.base_language import BaseLanguageModel
from langchain.memory.chat_memory import BaseChatMemory
from langchain.schema import BaseLanguageModel
from langchain.sql_database import SQLDatabase
from langchain.tools.python.tool import PythonAstREPLTool
from langchain.tools.sql_database.prompt import QUERY_CHECKER
class JsonAgent(AgentExecutor):
class CustomAgentExecutor(AgentExecutor, ABC):
"""Custom agent executor"""
@staticmethod
def function_name():
return "CustomAgentExecutor"
@classmethod
def initialize(cls, *args, **kwargs):
pass
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def run(self, *args, **kwargs):
return super().run(*args, **kwargs)
class JsonAgent(CustomAgentExecutor):
"""Json agent"""
@staticmethod
@ -51,7 +69,7 @@ class JsonAgent(AgentExecutor):
@classmethod
def from_toolkit_and_llm(cls, toolkit: JsonToolkit, llm: BaseLanguageModel):
tools = toolkit.get_tools()
tool_names = [tool.name for tool in tools]
tool_names = {tool.name for tool in tools}
prompt = ZeroShotAgent.create_prompt(
tools,
prefix=JSON_PREFIX,
@ -63,14 +81,14 @@ class JsonAgent(AgentExecutor):
llm=llm,
prompt=prompt,
)
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names)
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names) # type: ignore
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
def run(self, *args, **kwargs):
return super().run(*args, **kwargs)
class CSVAgent(AgentExecutor):
class CSVAgent(CustomAgentExecutor):
"""CSV agent"""
@staticmethod
@ -109,8 +127,8 @@ class CSVAgent(AgentExecutor):
llm=llm,
prompt=partial_prompt,
)
tool_names = [tool.name for tool in tools]
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs)
tool_names = {tool.name for tool in tools}
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
@ -118,7 +136,7 @@ class CSVAgent(AgentExecutor):
return super().run(*args, **kwargs)
class VectorStoreAgent(AgentExecutor):
class VectorStoreAgent(CustomAgentExecutor):
"""Vector Store agent"""
@staticmethod
@ -134,7 +152,7 @@ class VectorStoreAgent(AgentExecutor):
@classmethod
def from_toolkit_and_llm(
cls, llm: BaseLLM, vectorstoreinfo: VectorStoreInfo, **kwargs: Any
cls, llm: BaseLanguageModel, vectorstoreinfo: VectorStoreInfo, **kwargs: Any
):
"""Construct a vectorstore agent from an LLM and tools."""
@ -146,8 +164,8 @@ class VectorStoreAgent(AgentExecutor):
llm=llm,
prompt=prompt,
)
tool_names = [tool.name for tool in tools]
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs)
tool_names = {tool.name for tool in tools}
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
return AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, verbose=True
)
@ -156,7 +174,7 @@ class VectorStoreAgent(AgentExecutor):
return super().run(*args, **kwargs)
class SQLAgent(AgentExecutor):
class SQLAgent(CustomAgentExecutor):
"""SQL agent"""
@staticmethod
@ -171,10 +189,12 @@ class SQLAgent(AgentExecutor):
super().__init__(*args, **kwargs)
@classmethod
def from_toolkit_and_llm(cls, llm: BaseLLM, database_uri: str, **kwargs: Any):
def from_toolkit_and_llm(
cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any
):
"""Construct a sql agent from an LLM and tools."""
db = SQLDatabase.from_uri(database_uri)
toolkit = SQLDatabaseToolkit(db=db)
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
# The right code should be this, but there is a problem with tools = toolkit.get_tools()
# related to `OPENAI_API_KEY`
@ -212,8 +232,8 @@ class SQLAgent(AgentExecutor):
llm=llm,
prompt=prompt,
)
tool_names = [tool.name for tool in tools] # type: ignore
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs)
tool_names = {tool.name for tool in tools} # type: ignore
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
return AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools, # type: ignore
@ -226,7 +246,7 @@ class SQLAgent(AgentExecutor):
return super().run(*args, **kwargs)
class VectorStoreRouterAgent(AgentExecutor):
class VectorStoreRouterAgent(CustomAgentExecutor):
"""Vector Store Router Agent"""
@staticmethod
@ -255,8 +275,8 @@ class VectorStoreRouterAgent(AgentExecutor):
llm=llm,
prompt=prompt,
)
tool_names = [tool.name for tool in tools]
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs)
tool_names = {tool.name for tool in tools}
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
return AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, verbose=True
)
@ -265,7 +285,7 @@ class VectorStoreRouterAgent(AgentExecutor):
return super().run(*args, **kwargs)
class InitializeAgent(AgentExecutor):
class InitializeAgent(CustomAgentExecutor):
"""Implementation of initialize_agent function"""
@staticmethod
@ -274,7 +294,11 @@ class InitializeAgent(AgentExecutor):
@classmethod
def initialize(
cls, llm: BaseLLM, tools: List[Tool], agent: str, memory: BaseChatMemory
cls,
llm: BaseLanguageModel,
tools: List[Tool],
agent: str,
memory: Optional[BaseChatMemory] = None,
):
return initialize_agent(
tools=tools,

View file

@ -3,7 +3,7 @@ from langchain.agents import AgentExecutor, ZeroShotAgent
from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX
from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit
from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS
from langchain.schema import BaseLanguageModel
from langchain.base_language import BaseLanguageModel
class MalfoyAgent(AgentExecutor):
@ -21,7 +21,7 @@ class MalfoyAgent(AgentExecutor):
@classmethod
def from_toolkit_and_llm(cls, toolkit: JsonToolkit, llm: BaseLanguageModel):
tools = toolkit.get_tools()
tool_names = [tool.name for tool in tools]
tool_names = {tool.name for tool in tools}
prompt = ZeroShotAgent.create_prompt(
tools,
prefix=JSON_PREFIX,
@ -33,7 +33,7 @@ class MalfoyAgent(AgentExecutor):
llm=llm,
prompt=prompt,
)
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names)
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names) # type: ignore
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
def run(self, *args, **kwargs):

View file

@ -37,7 +37,9 @@ class ChainCreator(LangChainTypeCreator):
try:
if name in get_custom_nodes(self.type_name).keys():
return get_custom_nodes(self.type_name)[name]
return build_template_from_class(name, self.type_to_loader_dict)
return build_template_from_class(
name, self.type_to_loader_dict, add_function=True
)
except ValueError as exc:
raise ValueError("Chain not found") from exc
except AttributeError as exc:

View file

@ -9,10 +9,12 @@ from langchain import (
memory,
requests,
text_splitter,
utilities,
vectorstores,
)
from langchain.agents import agent_toolkits
from langchain.chat_models import ChatOpenAI
from langchain.sql_database import SQLDatabase
from langflow.interface.importing.utils import import_class
@ -76,3 +78,9 @@ documentloaders_type_to_cls_dict: dict[str, Any] = {
textsplitter_type_to_cls_dict: dict[str, Any] = dict(
inspect.getmembers(text_splitter, inspect.isclass)
)
## Utilities
utility_type_to_cls_dict: dict[str, Any] = dict(
inspect.getmembers(utilities, inspect.isclass)
)
utility_type_to_cls_dict["SQLDatabase"] = SQLDatabase

View file

@ -5,13 +5,11 @@ from typing import Any, Type
from langchain import PromptTemplate
from langchain.agents import Agent
from langchain.base_language import BaseLanguageModel
from langchain.chains.base import Chain
from langchain.chat_models.base import BaseChatModel
from langchain.llms.base import BaseLLM
from langchain.tools import BaseTool
from langflow.interface.tools.util import get_tool_by_name
def import_module(module_path: str) -> Any:
"""Import module from module path"""
@ -44,6 +42,7 @@ def import_by_type(_type: str, name: str) -> Any:
"vectorstores": import_vectorstore,
"documentloaders": import_documentloader,
"textsplitters": import_textsplitter,
"utilities": import_utility,
}
if _type == "llms":
key = "chat" if "chat" in name.lower() else "llm"
@ -99,15 +98,19 @@ def import_agent(agent: str) -> Agent:
return import_class(f"langchain.agents.{agent}")
def import_llm(llm: str) -> BaseLLM:
def import_llm(llm: str) -> BaseLanguageModel:
"""Import llm from llm name"""
return import_class(f"langchain.llms.{llm}")
def import_tool(tool: str) -> BaseTool:
"""Import tool from tool name"""
from langflow.interface.tools.base import tool_creator
return get_tool_by_name(tool)
if tool in tool_creator.type_to_loader_dict:
return tool_creator.type_to_loader_dict[tool]["fcn"]
return import_class(f"langchain.tools.{tool}")
def import_chain(chain: str) -> Type[Chain]:
@ -131,10 +134,16 @@ def import_vectorstore(vectorstore: str) -> Any:
def import_documentloader(documentloader: str) -> Any:
"""Import documentloader from documentloader name"""
return import_class(f"langchain.document_loaders.{documentloader}")
def import_textsplitter(textsplitter: str) -> Any:
"""Import textsplitter from textsplitter name"""
return import_class(f"langchain.text_splitter.{textsplitter}")
def import_utility(utility: str) -> Any:
"""Import utility from utility name"""
if utility == "SQLDatabase":
return import_class(f"langchain.sql_database.{utility}")
return import_class(f"langchain.utilities.{utility}")

View file

@ -8,6 +8,7 @@ from langflow.interface.prompts.base import prompt_creator
from langflow.interface.text_splitters.base import textsplitter_creator
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.tools.base import tool_creator
from langflow.interface.utilities.base import utility_creator
from langflow.interface.vector_store.base import vectorstore_creator
from langflow.interface.wrappers.base import wrapper_creator
@ -26,6 +27,7 @@ def get_type_dict():
"vectorStore": vectorstore_creator.to_list(),
"embeddings": embedding_creator.to_list(),
"textSplitters": textsplitter_creator.to_list(),
"utilities": utility_creator.to_list(),
}

View file

@ -13,13 +13,15 @@ from langchain.agents.load_tools import (
)
from langchain.agents.loading import load_agent_from_config
from langchain.agents.tools import Tool
from langchain.base_language import BaseLanguageModel
from langchain.callbacks.base import BaseCallbackManager
from langchain.chains.loading import load_chain_from_config
from langchain.llms.base import BaseLLM
from langchain.llms.loading import load_llm_from_config
from pydantic import ValidationError
from langflow.interface.agents.custom import CUSTOM_AGENTS
from langflow.interface.importing.utils import import_by_type
from langflow.interface.run import fix_memory_inputs
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.types import get_type_list
from langflow.interface.utils import load_file_into_dict
@ -28,61 +30,126 @@ from langflow.utils import util, validate
def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
"""Instantiate class from module type and key, and params"""
params = convert_params_to_sets(params)
if node_type in CUSTOM_AGENTS:
if custom_agent := CUSTOM_AGENTS.get(node_type):
return custom_agent.initialize(**params) # type: ignore
custom_agent = CUSTOM_AGENTS.get(node_type)
if custom_agent:
return custom_agent.initialize(**params)
class_object = import_by_type(_type=base_type, name=node_type)
return instantiate_based_on_type(class_object, base_type, node_type, params)
def convert_params_to_sets(params):
"""Convert certain params to sets"""
if "allowed_special" in params:
params["allowed_special"] = set(params["allowed_special"])
if "disallowed_special" in params:
params["disallowed_special"] = set(params["disallowed_special"])
return params
def instantiate_based_on_type(class_object, base_type, node_type, params):
if base_type == "agents":
# We need to initialize it differently
return load_agent_executor(class_object, params)
return instantiate_agent(class_object, params)
elif base_type == "prompts":
if node_type == "ZeroShotPrompt":
if "tools" not in params:
params["tools"] = []
return ZeroShotAgent.create_prompt(**params)
return instantiate_prompt(node_type, params)
elif base_type == "tools":
if node_type == "JsonSpec":
params["dict_"] = load_file_into_dict(params.pop("path"))
return class_object(**params)
elif node_type == "PythonFunction":
# If the node_type is "PythonFunction"
# we need to get the function from the params
# which will be a str containing a python function
# and then we need to compile it and return the function
# as the instance
function_string = params["code"]
if isinstance(function_string, str):
return validate.eval_function(function_string)
raise ValueError("Function should be a string")
return instantiate_tool(node_type, class_object, params)
elif base_type == "toolkits":
loaded_toolkit = class_object(**params)
# Check if node_type has a loader
if toolkits_creator.has_create_function(node_type):
return load_toolkits_executor(node_type, loaded_toolkit, params)
return loaded_toolkit
return instantiate_toolkit(node_type, class_object, params)
elif base_type == "embeddings":
params.pop("model")
return class_object(**params)
return instantiate_embedding(class_object, params)
elif base_type == "vectorstores":
return class_object.from_documents(**params)
return instantiate_vectorstore(class_object, params)
elif base_type == "documentloaders":
return class_object(**params).load()
return instantiate_documentloader(class_object, params)
elif base_type == "textsplitters":
documents = params.pop("documents")
text_splitter = class_object(**params)
return text_splitter.split_documents(documents)
return instantiate_textsplitter(class_object, params)
elif base_type == "utilities":
return instantiate_utility(node_type, class_object, params)
else:
return class_object(**params)
def instantiate_agent(class_object, params):
return load_agent_executor(class_object, params)
def instantiate_prompt(node_type, params):
if node_type == "ZeroShotPrompt":
if "tools" not in params:
params["tools"] = []
return ZeroShotAgent.create_prompt(**params)
return None # Or some other default action
def instantiate_tool(node_type, class_object, params):
if node_type == "JsonSpec":
params["dict_"] = load_file_into_dict(params.pop("path"))
return class_object(**params)
elif node_type == "PythonFunction":
function_string = params["code"]
if isinstance(function_string, str):
return validate.eval_function(function_string)
raise ValueError("Function should be a string")
elif node_type.lower() == "tool":
return class_object(**params)
return None # Or some other default action
def instantiate_toolkit(node_type, class_object, params):
loaded_toolkit = class_object(**params)
if toolkits_creator.has_create_function(node_type):
return load_toolkits_executor(node_type, loaded_toolkit, params)
return loaded_toolkit
def instantiate_embedding(class_object, params):
params.pop("model", None)
try:
return class_object(**params)
except ValidationError:
params = {
key: value
for key, value in params.items()
if key in class_object.__fields__
}
return class_object(**params)
def instantiate_vectorstore(class_object, params):
if len(params.get("documents", [])) == 0:
raise ValueError(
"The source you provided did not load correctly or was empty."
"This may cause an error in the vectorstore."
)
return class_object.from_documents(**params)
def instantiate_documentloader(class_object, params):
return class_object(**params).load()
def instantiate_textsplitter(class_object, params):
documents = params.pop("documents")
text_splitter = class_object(**params)
return text_splitter.split_documents(documents)
def instantiate_utility(node_type, class_object, params):
if node_type == "SQLDatabase":
return class_object.from_uri(params.pop("uri"))
return class_object(**params)
def load_flow_from_json(path: str):
def load_flow_from_json(path: str, build=True):
# This is done to avoid circular imports
from langflow.graph import Graph
"""Load flow from json file"""
with open(path, "r") as f:
with open(path, "r", encoding="utf-8") as f:
flow_graph = json.load(f)
data_graph = flow_graph["data"]
nodes = data_graph["nodes"]
@ -94,7 +161,19 @@ def load_flow_from_json(path: str):
# Nodes, edges and root node
edges = data_graph["edges"]
graph = Graph(nodes, edges)
return graph.build()
if build:
langchain_object = graph.build()
if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True
if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068
# Deactivating until we have a frontend solution
# to display intermediate steps
langchain_object.return_intermediate_steps = False
fix_memory_inputs(langchain_object)
return langchain_object
return graph
def replace_zero_shot_prompt_with_prompt_template(nodes):
@ -132,7 +211,7 @@ def load_langchain_type_from_config(config: Dict[str, Any]):
def load_agent_executor_from_config(
config: dict,
llm: Optional[BaseLLM] = None,
llm: Optional[BaseLanguageModel] = None,
tools: Optional[list[Tool]] = None,
callback_manager: Optional[BaseCallbackManager] = None,
**kwargs: Any,
@ -151,10 +230,15 @@ def load_agent_executor_from_config(
def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs):
"""Load agent executor from agent class, tools and chain"""
allowed_tools = params["allowed_tools"]
allowed_tools = params.get("allowed_tools", [])
llm_chain = params["llm_chain"]
# if allowed_tools is not a list or set, make it a list
if not isinstance(allowed_tools, (list, set)):
allowed_tools = [allowed_tools]
tool_names = [tool.name for tool in allowed_tools]
agent = agent_class(allowed_tools=tool_names, llm_chain=llm_chain)
# Agent class requires an output_parser but Agent classes
# have a default output_parser.
agent = agent_class(allowed_tools=tool_names, llm_chain=llm_chain) # type: ignore
return AgentExecutor.from_agent_and_tools(
agent=agent,
tools=allowed_tools,

View file

@ -2,9 +2,11 @@ import contextlib
import io
from typing import Any, Dict
from langflow.cache.utils import compute_dict_hash, load_cache, memoize_dict
from chromadb.errors import NotEnoughElementsException # type: ignore
from langflow.api.callback import AsyncStreamingLLMCallbackHandler, StreamingLLMCallbackHandler # type: ignore
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict
from langflow.graph.graph import Graph
from langflow.interface import loading
from langflow.utils.logger import logger
@ -31,23 +33,23 @@ def load_or_build_langchain_object(data_graph, is_first_message=False):
return build_langchain_object_with_caching(data_graph)
@memoize_dict(maxsize=1)
@memoize_dict(maxsize=10)
def build_langchain_object_with_caching(data_graph):
"""
Build langchain object from data_graph.
"""
logger.debug("Building langchain object")
nodes = data_graph["nodes"]
# Add input variables
# nodes = payload.extract_input_variables(nodes)
# Nodes, edges and root node
edges = data_graph["edges"]
graph = Graph(nodes, edges)
graph = build_graph(data_graph)
return graph.build()
def build_graph(data_graph):
nodes = data_graph["nodes"]
edges = data_graph["edges"]
return Graph(nodes, edges)
def build_langchain_object(data_graph):
"""
Build langchain object from data_graph.
@ -64,47 +66,12 @@ def build_langchain_object(data_graph):
return graph.build()
def process_graph(data_graph: Dict[str, Any]):
def process_graph_cached(data_graph: Dict[str, Any], message: str):
"""
Process graph by extracting input variables and replacing ZeroShotPrompt
with PromptTemplate,then run the graph and return the result and thought.
"""
# Load langchain object
logger.debug("Loading langchain object")
message = data_graph.pop("message", "")
is_first_message = len(data_graph.get("chatHistory", [])) == 0
computed_hash, langchain_object = load_langchain_object(
data_graph, is_first_message
)
logger.debug("Loaded langchain object")
if langchain_object is None:
# Raise user facing error
raise ValueError(
"There was an error loading the langchain_object. Please, check all the nodes and try again."
)
# Generate result and thought
logger.debug("Generating result and thought")
result, thought = get_result_and_thought_using_graph(langchain_object, message)
logger.debug("Generated result and thought")
# Save langchain_object to cache
# We have to save it here because if the
# memory is updated we need to keep the new values
logger.debug("Saving langchain object to cache")
# save_cache(computed_hash, langchain_object, is_first_message)
logger.debug("Saved langchain object to cache")
return {"result": str(result), "thought": thought.strip()}
def process_graph_cached(data_graph: Dict[str, Any]):
"""
Process graph by extracting input variables and replacing ZeroShotPrompt
with PromptTemplate,then run the graph and return the result and thought.
"""
# Load langchain object
message = data_graph.pop("message", "")
is_first_message = len(data_graph.get("chatHistory", [])) == 0
langchain_object = load_or_build_langchain_object(data_graph, is_first_message)
logger.debug("Loaded langchain object")
@ -117,7 +84,7 @@ def process_graph_cached(data_graph: Dict[str, Any]):
# Generate result and thought
logger.debug("Generating result and thought")
result, thought = get_result_and_thought_using_graph(langchain_object, message)
result, thought = get_result_and_thought(langchain_object, message)
logger.debug("Generated result and thought")
return {"result": str(result), "thought": thought.strip()}
@ -170,10 +137,12 @@ def fix_memory_inputs(langchain_object):
if langchain_object.memory.memory_key in langchain_object.input_variables:
return
except AttributeError:
if (
langchain_object.memory.memory_key
in langchain_object.prompt.input_variables
):
input_variables = (
langchain_object.prompt.input_variables
if hasattr(langchain_object, "prompt")
else langchain_object.input_keys
)
if langchain_object.memory.memory_key in input_variables:
return
possible_new_mem_key = get_memory_key(langchain_object)
@ -181,7 +150,68 @@ def fix_memory_inputs(langchain_object):
update_memory_keys(langchain_object, possible_new_mem_key)
def get_result_and_thought_using_graph(langchain_object, message: str):
async def get_result_and_steps(langchain_object, message: str, **kwargs):
"""Get result and thought from extracted json"""
try:
if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True
chat_input = None
memory_key = ""
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
memory_key = langchain_object.memory.memory_key
if hasattr(langchain_object, "input_keys"):
for key in langchain_object.input_keys:
if key not in [memory_key, "chat_history"]:
chat_input = {key: message}
else:
chat_input = message # type: ignore
if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068
# Deactivating until we have a frontend solution
# to display intermediate steps
langchain_object.return_intermediate_steps = True
fix_memory_inputs(langchain_object)
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer):
try:
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
output = await langchain_object.acall(
chat_input, callbacks=async_callbacks
)
except Exception as exc:
# make the error message more informative
logger.debug(f"Error: {str(exc)}")
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
output = langchain_object(chat_input, callbacks=sync_callbacks)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []
)
result = (
output.get(langchain_object.output_keys[0])
if isinstance(output, dict)
else output
)
if intermediate_steps:
thought = format_intermediate_steps(intermediate_steps)
else:
thought = output_buffer.getvalue()
except NotEnoughElementsException as exc:
raise ValueError(
"Error: Not enough documents for ChromaDB to index. Try reducing chunk size in TextSplitter."
) from exc
except Exception as exc:
raise ValueError(f"Error: {str(exc)}") from exc
return result, thought
def get_result_and_thought(langchain_object, message: str):
"""Get result and thought from extracted json"""
try:
if hasattr(langchain_object, "verbose"):
@ -208,6 +238,9 @@ def get_result_and_thought_using_graph(langchain_object, message: str):
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer):
try:
# if hasattr(langchain_object, "acall"):
# output = await langchain_object.acall(chat_input)
# else:
output = langchain_object(chat_input)
except ValueError as exc:
# make the error message more informative
@ -233,34 +266,6 @@ def get_result_and_thought_using_graph(langchain_object, message: str):
return result, thought
def get_result_and_thought(extracted_json: Dict[str, Any], message: str):
"""Get result and thought from extracted json"""
try:
langchain_object = loading.load_langchain_type_from_config(
config=extracted_json
)
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer):
output = langchain_object(message)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []
)
result = (
output.get(langchain_object.output_keys[0])
if isinstance(output, dict)
else output
)
if intermediate_steps:
thought = format_intermediate_steps(intermediate_steps)
else:
thought = output_buffer.getvalue()
except Exception as e:
result = f"Error: {str(e)}"
thought = ""
return result, thought
def format_intermediate_steps(intermediate_steps):
formatted_chain = "> Entering new AgentExecutor chain...\n"
for step in intermediate_steps:

View file

@ -1,7 +1,6 @@
from typing import Dict, List, Optional
from langchain.agents.load_tools import (
_BASE_TOOLS,
_EXTRA_LLM_TOOLS,
_EXTRA_OPTIONAL_TOOLS,
_LLM_TOOLS,
@ -10,17 +9,16 @@ from langchain.agents.load_tools import (
from langflow.custom import customs
from langflow.interface.base import LangChainTypeCreator
from langflow.interface.tools.constants import (
ALL_TOOLS_NAMES,
CUSTOM_TOOLS,
FILE_TOOLS,
OTHER_TOOLS,
)
from langflow.interface.tools.util import (
get_tool_by_name,
get_tool_params,
get_tools_dict,
)
from langflow.interface.tools.util import get_tool_params
from langflow.settings import settings
from langflow.template.base import Template, TemplateField
from langflow.utils import util
from langflow.utils.util import build_template_from_class
TOOL_INPUTS = {
"str": TemplateField(
@ -31,7 +29,9 @@ TOOL_INPUTS = {
placeholder="",
value="",
),
"llm": TemplateField(field_type="BaseLLM", required=True, is_list=False, show=True),
"llm": TemplateField(
field_type="BaseLanguageModel", required=True, is_list=False, show=True
),
"func": TemplateField(
field_type="function",
required=True,
@ -66,75 +66,94 @@ class ToolCreator(LangChainTypeCreator):
@property
def type_to_loader_dict(self) -> Dict:
if self.tools_dict is None:
self.tools_dict = get_tools_dict()
all_tools = {}
for tool, tool_fcn in ALL_TOOLS_NAMES.items():
tool_params = get_tool_params(tool_fcn)
tool_name = tool_params.get("name", tool)
if tool_name in settings.tools or settings.dev:
if tool_name == "JsonSpec":
tool_params["path"] = tool_params.pop("dict_") # type: ignore
all_tools[tool_name] = {
"type": tool,
"params": tool_params,
"fcn": tool_fcn,
}
self.tools_dict = all_tools
return self.tools_dict
def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of a tool."""
base_classes = ["Tool"]
all_tools = {}
for tool in self.type_to_loader_dict.keys():
tool_fcn = get_tool_by_name(tool)
if tool_params := get_tool_params(tool_fcn):
tool_name = tool_params.get("name") or str(tool)
all_tools[tool_name] = {
"type": tool,
"params": tool_params,
"fcn": tool_fcn,
}
fields = []
params = []
tool_params = {}
# Raise error if name is not in tools
if name not in all_tools.keys():
if name not in self.type_to_loader_dict.keys():
raise ValueError("Tool not found")
tool_type: str = all_tools[name]["type"] # type: ignore
tool_type: str = self.type_to_loader_dict[name]["type"] # type: ignore
if all_tools[tool_type]["fcn"] in _BASE_TOOLS.values():
params = []
elif all_tools[tool_type]["fcn"] in _LLM_TOOLS.values():
# if tool_type in _BASE_TOOLS.keys():
# params = []
if tool_type in _LLM_TOOLS.keys():
params = ["llm"]
elif all_tools[tool_type]["fcn"] in [
val[0] for val in _EXTRA_LLM_TOOLS.values()
]:
n_dict = {val[0]: val[1] for val in _EXTRA_LLM_TOOLS.values()}
extra_keys = n_dict[all_tools[tool_type]["fcn"]]
elif tool_type in _EXTRA_LLM_TOOLS.keys():
extra_keys = _EXTRA_LLM_TOOLS[tool_type][1]
params = ["llm"] + extra_keys
elif all_tools[tool_type]["fcn"] in [
val[0] for val in _EXTRA_OPTIONAL_TOOLS.values()
]:
n_dict = {val[0]: val[1] for val in _EXTRA_OPTIONAL_TOOLS.values()} # type: ignore
extra_keys = n_dict[all_tools[tool_type]["fcn"]]
elif tool_type in _EXTRA_OPTIONAL_TOOLS.keys():
extra_keys = _EXTRA_OPTIONAL_TOOLS[tool_type][1]
params = extra_keys
elif tool_type == "Tool":
params = ["name", "description", "func"]
# elif tool_type == "Tool":
# params = ["name", "description", "func"]
elif tool_type in CUSTOM_TOOLS:
# Get custom tool params
params = all_tools[name]["params"] # type: ignore
params = self.type_to_loader_dict[name]["params"] # type: ignore
base_classes = ["function"]
if node := customs.get_custom_nodes("tools").get(tool_type):
return node
elif tool_type in FILE_TOOLS:
params = all_tools[name]["params"] # type: ignore
if tool_type == "JsonSpec":
params["path"] = params.pop("dict_") # type: ignore
params = self.type_to_loader_dict[name]["params"] # type: ignore
base_classes += [name]
else:
params = []
elif tool_type in OTHER_TOOLS:
tool_dict = build_template_from_class(tool_type, OTHER_TOOLS)
fields = tool_dict["template"]
# Pop unnecessary fields and add name
fields.pop("_type") # type: ignore
fields.pop("return_direct") # type: ignore
fields.pop("verbose") # type: ignore
tool_params = {
"name": fields.pop("name")["value"], # type: ignore
"description": fields.pop("description")["value"], # type: ignore
}
fields = [
TemplateField(name=name, field_type=field["type"], **field)
for name, field in fields.items() # type: ignore
]
base_classes += tool_dict["base_classes"]
# Copy the field and add the name
fields = []
for param in params:
field = TOOL_INPUTS.get(param, TOOL_INPUTS["str"]).copy()
field.name = param
field.advanced = False
if param == "aiosession":
field.show = False
field.required = False
fields.append(field)
template = Template(fields=fields, type_name=tool_type)
tool_params = all_tools[name]["params"]
tool_params = {**tool_params, **self.type_to_loader_dict[name]["params"]}
return {
"template": util.format_dict(template.to_dict()),
**tool_params,
@ -144,21 +163,7 @@ class ToolCreator(LangChainTypeCreator):
def to_list(self) -> List[str]:
"""List all load tools"""
tools = []
for tool, fcn in get_tools_dict().items():
tool_params = get_tool_params(fcn)
if tool_params and not tool_params.get("name"):
tool_params["name"] = tool
if tool_params and (
tool_params.get("name") in settings.tools
or (tool_params.get("name") and settings.dev)
):
tools.append(tool_params["name"])
return tools
return list(self.type_to_loader_dict.keys())
tool_creator = ToolCreator()

View file

@ -1,3 +1,4 @@
from langchain import tools
from langchain.agents import Tool
from langchain.agents.load_tools import (
_BASE_TOOLS,
@ -7,10 +8,14 @@ from langchain.agents.load_tools import (
)
from langchain.tools.json.tool import JsonSpec
from langflow.interface.importing.utils import import_class
from langflow.interface.tools.custom import PythonFunction
FILE_TOOLS = {"JsonSpec": JsonSpec}
CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction}
OTHER_TOOLS = {tool: import_class(f"langchain.tools.{tool}") for tool in tools.__all__}
ALL_TOOLS_NAMES = {
**_BASE_TOOLS,
**_LLM_TOOLS, # type: ignore
@ -18,4 +23,5 @@ ALL_TOOLS_NAMES = {
**{k: v[0] for k, v in _EXTRA_OPTIONAL_TOOLS.items()},
**CUSTOM_TOOLS,
**FILE_TOOLS, # type: ignore
**OTHER_TOOLS,
}

View file

@ -4,29 +4,6 @@ from typing import Dict, Union
from langchain.agents.tools import Tool
from langflow.interface.tools.constants import ALL_TOOLS_NAMES
def get_tools_dict():
"""Get the tools dictionary."""
all_tools = {}
for tool, fcn in ALL_TOOLS_NAMES.items():
if tool_params := get_tool_params(fcn):
tool_name = tool_params.get("name") or str(tool)
all_tools[tool_name] = fcn
return all_tools
def get_tool_by_name(name: str):
"""Get a tool from the tools dictionary."""
tools = get_tools_dict()
if name not in tools:
raise ValueError(f"{name} not found.")
return tools[name]
def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
tree = ast.parse(inspect.getsource(func))
@ -42,11 +19,19 @@ def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
tool_params = {}
for keyword in tool.keywords:
if keyword.arg == "name":
tool_params["name"] = ast.literal_eval(keyword.value)
try:
tool_params["name"] = ast.literal_eval(
keyword.value
)
except ValueError:
break
elif keyword.arg == "description":
tool_params["description"] = ast.literal_eval(
keyword.value
)
try:
tool_params["description"] = ast.literal_eval(
keyword.value
)
except ValueError:
continue
return tool_params
return {
@ -113,6 +98,8 @@ def get_tool_params(tool, **kwargs) -> Dict:
elif inspect.isclass(tool):
# Get the parameters necessary to
# instantiate the class
return get_class_tool_params(tool, **kwargs) or {}
else:
raise ValueError("Tool must be a function or class.")

View file

@ -8,6 +8,7 @@ from langflow.interface.prompts.base import prompt_creator
from langflow.interface.text_splitters.base import textsplitter_creator
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.tools.base import tool_creator
from langflow.interface.utilities.base import utility_creator
from langflow.interface.vector_store.base import vectorstore_creator
from langflow.interface.wrappers.base import wrapper_creator
@ -42,6 +43,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
vectorstore_creator,
documentloader_creator,
textsplitter_creator,
utility_creator,
]
all_types = {}

View file

@ -0,0 +1,39 @@
from typing import Dict, List, Optional
from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import utility_type_to_cls_dict
from langflow.settings import settings
from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class
class UtilityCreator(LangChainTypeCreator):
type_name: str = "utilities"
@property
def type_to_loader_dict(self) -> Dict:
return utility_type_to_cls_dict
def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of a utility."""
try:
if name in get_custom_nodes(self.type_name).keys():
return get_custom_nodes(self.type_name)[name]
return build_template_from_class(name, utility_type_to_cls_dict)
except ValueError as exc:
raise ValueError(f"Utility {name} not found") from exc
except AttributeError as exc:
logger.error(f"Utility {name} not loaded: {exc}")
return None
def to_list(self) -> List[str]:
return [
utility.__name__
for utility in self.type_to_loader_dict.values()
if utility.__name__ in settings.utilities or settings.dev
]
utility_creator = UtilityCreator()

View file

@ -1,7 +1,11 @@
import base64
import json
import os
from io import BytesIO
import yaml
from langchain.base_language import BaseLanguageModel
from PIL.Image import Image
def load_file_into_dict(file_path: str) -> dict:
@ -20,3 +24,27 @@ def load_file_into_dict(file_path: str) -> dict:
raise ValueError("Unsupported file type. Please provide a JSON or YAML file.")
return data
def pil_to_base64(image: Image) -> str:
buffered = BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue())
return img_str.decode("utf-8")
def try_setting_streaming_options(langchain_object, websocket):
# If the LLM type is OpenAI or ChatOpenAI,
# set streaming to True
# First we need to find the LLM
llm = None
if hasattr(langchain_object, "llm"):
llm = langchain_object.llm
elif hasattr(langchain_object, "llm_chain") and hasattr(
langchain_object.llm_chain, "llm"
):
llm = langchain_object.llm_chain.llm
if isinstance(llm, BaseLanguageModel):
llm.streaming = bool(hasattr(llm, "streaming"))
return langchain_object

View file

@ -1,6 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langflow.api.chat import router as chat_router
from langflow.api.endpoints import router as endpoints_router
from langflow.api.validate import router as validate_router
@ -23,6 +24,7 @@ def create_app():
app.include_router(endpoints_router)
app.include_router(validate_router)
app.include_router(chat_router)
return app

View file

@ -18,6 +18,7 @@ class Settings(BaseSettings):
wrappers: List[str] = []
toolkits: List[str] = []
textsplitters: List[str] = []
utilities: List[str] = []
dev: bool = False
class Config:
@ -42,6 +43,7 @@ class Settings(BaseSettings):
self.wrappers = new_settings.wrappers or []
self.toolkits = new_settings.toolkits or []
self.textsplitters = new_settings.textsplitters or []
self.utilities = new_settings.utilities or []
self.dev = new_settings.dev or False

View file

@ -23,6 +23,7 @@ class TemplateFieldCreator(BaseModel, ABC):
options: list[str] = []
name: str = ""
display_name: Optional[str] = None
advanced: bool = False
def to_dict(self):
result = self.dict()
@ -161,14 +162,23 @@ class FrontendNode(BaseModel):
_type = _type.replace("Optional[", "")[:-1]
# Check for list type
if "List" in _type:
_type = _type.replace("List[", "")[:-1]
if "List" in _type or "Sequence" in _type:
_type = _type.replace("List[", "")
_type = _type.replace("Sequence[", "")[:-1]
field.is_list = True
# Replace 'Mapping' with 'dict'
if "Mapping" in _type:
_type = _type.replace("Mapping", "dict")
# {'type': 'Union[float, Tuple[float, float], NoneType]'} != {'type': 'float'}
if "Union" in _type:
_type = _type.replace("Union[", "")[:-1]
_type = _type.split(",")[0]
_type = _type.replace("]", "").replace("[", "")
field.field_type = _type
# Change type from str to Tool
field.field_type = "Tool" if key in {"allowed_tools"} else field.field_type
@ -196,6 +206,7 @@ class FrontendNode(BaseModel):
"examples",
"code",
"headers",
"description",
}
# Replace dict type with str
@ -225,6 +236,16 @@ class FrontendNode(BaseModel):
field.is_list = True
if "api_key" in key and "OpenAI" in str(name):
field.display_name = "OpenAI API Key"
field.required = True
field.required = False
if field.value is None:
field.value = ""
if "kwargs" in field.name.lower():
field.advanced = True
field.required = False
field.show = False
# If the field.name contains api or api and key, then it might be an api key
# other conditions are to make sure that it is not an input or output variable
if "api" in key.lower() and "key" in key.lower():
field.required = False
field.advanced = False

View file

@ -77,6 +77,12 @@ class PromptTemplateNode(FrontendNode):
def to_dict(self):
return super().to_dict()
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if field.name == "examples":
field.advanced = False
class PythonFunctionNode(FrontendNode):
name: str = "PythonFunction"
@ -91,6 +97,7 @@ class PythonFunctionNode(FrontendNode):
show=True,
value=DEFAULT_PYTHON_FUNCTION,
name="code",
advanced=False,
)
],
)
@ -101,10 +108,126 @@ class PythonFunctionNode(FrontendNode):
return super().to_dict()
class MidJourneyPromptChainNode(FrontendNode):
name: str = "MidJourneyPromptChain"
template: Template = Template(
type_name="MidJourneyPromptChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
],
)
description: str = "MidJourneyPromptChain is a chain you can use to generate new MidJourney prompts."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"MidJourneyPromptChain",
]
class TimeTravelGuideChainNode(FrontendNode):
name: str = "TimeTravelGuideChain"
template: Template = Template(
type_name="TimeTravelGuideChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
],
)
description: str = "Time travel guide chain to be used in the flow."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"TimeTravelGuideChain",
"Chain",
"ConversationChain",
]
class SeriesCharacterChainNode(FrontendNode):
name: str = "SeriesCharacterChain"
template: Template = Template(
type_name="SeriesCharacterChain",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="character",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="series",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
),
],
)
description: str = "SeriesCharacterChain is a chain you can use to have a conversation with a character from a series." # noqa
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"SeriesCharacterChain",
"function",
]
class ToolNode(FrontendNode):
name: str = "Tool"
template: Template = Template(
type_name="tool",
type_name="Tool",
fields=[
TemplateField(
field_type="str",
@ -115,6 +238,7 @@ class ToolNode(FrontendNode):
multiline=True,
value="",
name="name",
advanced=False,
),
TemplateField(
field_type="str",
@ -125,21 +249,31 @@ class ToolNode(FrontendNode):
multiline=True,
value="",
name="description",
advanced=False,
),
TemplateField(
field_type="str",
name="func",
field_type="function",
required=True,
is_list=False,
show=True,
multiline=True,
advanced=False,
),
TemplateField(
field_type="bool",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value="",
name="func",
multiline=False,
value=False,
name="return_direct",
),
],
)
description: str = "Tool to be used in the flow."
base_classes: list[str] = ["BaseTool"]
base_classes: list[str] = ["Tool"]
def to_dict(self):
return super().to_dict()
@ -185,12 +319,14 @@ class InitializeAgentNode(FrontendNode):
options=list(NON_CHAT_AGENTS.keys()),
value=list(NON_CHAT_AGENTS.keys())[0],
name="agent",
advanced=False,
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
TemplateField(
field_type="Tool",
@ -198,17 +334,19 @@ class InitializeAgentNode(FrontendNode):
show=True,
name="tools",
is_list=True,
advanced=False,
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
advanced=False,
),
],
)
description: str = """Construct a json agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor"]
base_classes: list[str] = ["AgentExecutor", "function"]
def to_dict(self):
return super().to_dict()
@ -248,6 +386,29 @@ class CSVAgentNode(FrontendNode):
return super().to_dict()
class SQLDatabaseNode(FrontendNode):
name: str = "SQLDatabase"
template: Template = Template(
type_name="sql_database",
fields=[
TemplateField(
field_type="str",
required=True,
is_list=False,
show=True,
multiline=False,
value="",
name="uri",
),
],
)
description: str = """SQLAlchemy wrapper around a database."""
base_classes: list[str] = ["SQLDatabase"]
def to_dict(self):
return super().to_dict()
class VectorStoreAgentNode(FrontendNode):
name: str = "VectorStoreAgent"
template: Template = Template(
@ -345,6 +506,7 @@ class PromptFrontendNode(FrontendNode):
"suffix",
"prefix",
"examples",
"format_instructions",
]
if field.field_type == "StringPromptTemplate" and "Message" in str(name):
field.field_type = "prompt"
@ -355,6 +517,7 @@ class PromptFrontendNode(FrontendNode):
if field.name in PROMPT_FIELDS:
field.field_type = "prompt"
field.advanced = False
if (
"Union" in field.field_type
@ -387,17 +550,33 @@ class ChainFrontendNode(FrontendNode):
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
field.advanced = False
if "key" in field.name:
field.password = False
field.show = False
if field.name in ["input_key", "output_key"]:
field.required = True
field.show = True
field.advanced = True
# Separated for possible future changes
if field.name == "prompt":
if field.name == "prompt" and field.value is None:
# if no prompt is provided, use the default prompt
field.required = False
field.show = True
field.advanced = False
if field.name == "memory":
field.required = False
field.show = True
field.advanced = False
if field.name == "verbose":
field.required = False
field.show = True
field.advanced = True
if field.name == "llm":
field.required = True
field.show = True
field.advanced = False
class LLMFrontendNode(FrontendNode):
@ -407,22 +586,31 @@ class LLMFrontendNode(FrontendNode):
"huggingfacehub_api_token": "HuggingFace Hub API Token",
}
FrontendNode.format_field(field, name)
SHOW_FIELDS = ["repo_id", "task", "model_kwargs"]
SHOW_FIELDS = ["repo_id"]
if field.name in SHOW_FIELDS:
field.show = True
if "api" in field.name and ("key" in field.name or "token" in field.name):
field.password = True
field.show = True
field.required = True
# Required should be False to support
# loading the API key from environment variables
field.required = False
field.advanced = False
if field.name == "task":
field.required = True
field.show = True
field.is_list = True
field.options = ["text-generation", "text2text-generation"]
field.advanced = True
if display_name := display_names_dict.get(field.name):
field.display_name = display_name
if field.name == "model_kwargs":
field.field_type = "code"
field.advanced = True
field.show = True
elif field.name in ["model_name", "temperature"]:
field.advanced = False
field.show = True

View file

@ -1,6 +1,7 @@
import importlib
import inspect
import re
from functools import wraps
from typing import Dict, Optional
from docstring_parser import parse # type: ignore
@ -216,7 +217,7 @@ def format_dict(d, name: Optional[str] = None):
_type = _type.replace("Optional[", "")[:-1]
# Check for list type
if "List" in _type:
if "List" in _type or "Sequence" in _type or "Set" in _type:
_type = _type.replace("List[", "")[:-1]
value["list"] = True
else:
@ -301,3 +302,15 @@ def update_verbose(d: dict, new_value: bool) -> dict:
elif k == "verbose":
d[k] = new_value
return d
def sync_to_async(func):
"""
Decorator to convert a sync function to an async function.
"""
@wraps(func)
async def async_wrapper(*args, **kwargs):
return func(*args, **kwargs)
return async_wrapper

View file

@ -59,7 +59,12 @@ def eval_function(function_string: str):
# Execute the code string in the new namespace
exec(function_string, namespace)
function_object = next(
(obj for name, obj in namespace.items() if isinstance(obj, types.FunctionType)),
(
obj
for name, obj in namespace.items()
if isinstance(obj, types.FunctionType)
and obj.__code__.co_filename == "<string>"
),
None,
)
if function_object is None:

View file

@ -1,8 +0,0 @@
#! /bin/bash
poetry remove langchain
docker build -t logspace/backend_build -f build.Dockerfile .
VERSION=$(toml get --toml-path pyproject.toml tool.poetry.version)
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow:$VERSION .
docker run -p 5003:80 -d ibiscp/langflow:$VERSION
poetry add --editable ../../../langchain

View file

@ -1,5 +0,0 @@
FROM node:14-alpine
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build

View file

@ -1,11 +0,0 @@
#! /bin/bash
# Read the contents of the JSON file
json=$(cat package.json)
# Extract the value of the "version" field using jq
VERSION=$(echo "$json" | jq -r '.version')
docker build -t logspace/frontend_build -f build.Dockerfile .
docker build --build-arg VERSION=$VERSION -t ibiscp/langflow_frontend:$VERSION .
docker push ibiscp/langflow_frontend:$VERSION

View file

@ -15,7 +15,10 @@ WORKDIR /home/node/app
COPY --chown=node:node . ./
COPY ./set_proxy.sh .
RUN chmod +x set_proxy.sh && ./set_proxy.sh
RUN chmod +x set_proxy.sh && \
cat set_proxy.sh | tr -d '\r' > set_proxy_unix.sh && \
chmod +x set_proxy_unix.sh && \
./set_proxy_unix.sh
USER node

View file

@ -4,12 +4,12 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<title>LangFlow</title>
</head>
<body id='body' style="width: 100%; height:100%">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div style="width: 100vw; height:100vh" id='root'>
</div>
<div style="width: 100vw; height:100vh" id='root'></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -8,38 +8,36 @@
"@headlessui/react": "^1.7.10",
"@heroicons/react": "^2.0.15",
"@mui/material": "^5.11.9",
"@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.12",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@tailwindcss/line-clamp": "^0.4.4",
"ace-builds": "^1.16.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.3.2",
"base64-js": "^1.5.1",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.2",
"react-icons": "^4.7.1",
"react-icons": "^4.8.0",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.8.1",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.0",
"reactflow": "^11.5.5",
"tailwindcss": "^3.2.6",
"typescript": "^4.9.5",
"rehype-mathjax": "^4.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "vite",
"build": "vite build",
"serve": "vite preview"
},
"eslintConfig": {
"extends": [
@ -59,5 +57,23 @@
"last 1 safari version"
]
},
"proxy": "http://backend:7860"
}
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.194",
"@types/node": "^16.18.12",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.1",
"@vitejs/plugin-react-swc": "^3.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.2",
"vite": "^4.3.5"
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -7,4 +7,4 @@ packagejson=$(cat package.json)
packagejson=$(echo "$packagejson" | jq ".proxy = \"$backend_url\"")
echo "$packagejson" > package.json
echo "$packagejson" > package.json

View file

@ -3,45 +3,45 @@
@tailwind utilities;
.App {
text-align: center;
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@font-face{
font-family: text-security-disc;
src: url("assets/text-security-disc.woff") format("woff");
}
@font-face {
font-family: text-security-disc;
src: url("assets/text-security-disc.woff") format("woff");
}

View file

@ -2,6 +2,8 @@ import "reactflow/dist/style.css";
import { useState, useEffect, useContext } from "react";
import "./App.css";
import { useLocation } from "react-router-dom";
import _ from "lodash";
import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
@ -14,7 +16,6 @@ import CrashErrorComponent from "./components/CrashErrorComponent";
import { TabsContext } from "./contexts/tabsContext";
export default function App() {
var _ = require("lodash");
let { setCurrent, setShowSideBar, setIsStackedOpen } =
useContext(locationContext);
@ -24,7 +25,7 @@ export default function App() {
setShowSideBar(true);
setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const {hardReset} = useContext(TabsContext)
const { hardReset } = useContext(TabsContext);
const {
errorData,
errorOpen,
@ -108,7 +109,7 @@ export default function App() {
onReset={() => {
window.localStorage.removeItem("tabsData");
window.localStorage.clear();
hardReset()
hardReset();
window.location.href = window.location.href;
}}
FallbackComponent={CrashErrorComponent}
@ -158,7 +159,7 @@ export default function App() {
<a
target={"_blank"}
href="https://logspace.ai/"
className="absolute bottom-1 left-1 text-gray-500 text-xs cursor-pointer font-sans tracking-wide"
className="absolute bottom-2 left-6 text-gray-300 px-2 rounded-lg text-xs cursor-pointer font-sans tracking-wide bg-gray-800 dark:bg-gray-300 dark:text-gray-800 "
>
Created by Logspace
</a>

View file

@ -198,7 +198,9 @@ export default function ParameterComponent({
save();
}}
/>
):(<></>)}
) : (
<></>
)}
</>
</div>
);

View file

@ -1,16 +1,16 @@
import { TrashIcon } from "@heroicons/react/24/outline";
import {
classNames,
nodeColors,
nodeIcons,
snakeToNormalCase,
} from "../../utils";
import { BugAntIcon, Cog6ToothIcon, ExclamationCircleIcon, InformationCircleIcon, TrashIcon } from "@heroicons/react/24/outline";
import { classNames, nodeColors, nodeIcons, toNormalCase } from "../../utils";
import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext";
import { useContext, useRef } from "react";
import { useContext, useState, useEffect, useRef, Fragment } from "react";
import { NodeDataType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext";
import NodeModal from "../../modals/NodeModal";
import { useCallback } from "react";
import { TabsContext } from "../../contexts/tabsContext";
import { debounce } from "../../utils";
import Tooltip from "../../components/TooltipComponent";
export default function GenericNode({
data,
selected,
@ -18,10 +18,66 @@ export default function GenericNode({
data: NodeDataType;
selected: boolean;
}) {
const { setErrorData } = useContext(alertContext);
const showError = useRef(true);
const { types, deleteNode } = useContext(typesContext);
const Icon = nodeIcons[types[data.type]];
const { setErrorData } = useContext(alertContext);
const showError = useRef(true);
const { types, deleteNode } = useContext(typesContext);
const { openPopUp } = useContext(PopUpContext);
const Icon = nodeIcons[types[data.type]];
const [validationStatus, setValidationStatus] = useState(null);
// State for outline color
const [isValid, setIsValid] = useState(false);
const { save } = useContext(TabsContext);
const { reactFlowInstance } = useContext(typesContext);
const [params, setParams] = useState([]);
useEffect(() => {
if (reactFlowInstance) {
setParams(Object.values(reactFlowInstance.toObject()));
}
}, [save]);
const validateNode = useCallback(
debounce(async () => {
try {
const response = await fetch(`/validate/node/${data.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reactFlowInstance.toObject()),
});
if (response.status === 200) {
let jsonResponse = await response.json();
let jsonResponseParsed = await JSON.parse(jsonResponse);
console.log(jsonResponseParsed);
if(jsonResponseParsed.valid){
setValidationStatus(jsonResponseParsed.params);
} else {
setValidationStatus("error");
}
}
} catch (error) {
// console.error("Error validating node:", error);
setValidationStatus("error");
}
}, 1000), // Adjust the debounce delay (500ms) as needed
[reactFlowInstance, data.id]
);
useEffect(() => {
if (params.length > 0) {
validateNode();
}
}, [params, validateNode]);
useEffect(() => {
if (validationStatus !== "error") {
setIsValid(true);
} else {
setIsValid(false);
}
}, [validationStatus]);
if (!Icon) {
if (showError.current) {
setErrorData({
@ -34,49 +90,92 @@ export default function GenericNode({
deleteNode(data.id);
return;
}
return (
<div
className={classNames(
selected ? "border border-blue-500" : "border dark:border-gray-700",
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center"
)}
>
<div className="w-full dark:text-white flex items-center justify-between p-4 gap-8 bg-gray-50 rounded-t-lg dark:bg-gray-800 border-b dark:border-b-gray-700 ">
<div className="w-full flex items-center truncate gap-4 text-lg">
<Icon
className="w-10 h-10 p-1 rounded"
style={{
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
}}
/>
<div className="truncate">{data.type}</div>
</div>
<button
onClick={() => {
deleteNode(data.id);
}}
>
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
</button>
</div>
<div className="w-full h-full py-5">
<div className="w-full text-gray-500 px-5 text-sm">
{data.node.description}
</div>
return (
<div
className={classNames(
isValid ? "animate-pulse-green" : "border-red-outline",
selected ? "border border-blue-500" : "border dark:border-gray-700",
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center"
)}
>
<div className="w-full dark:text-white flex items-center justify-between p-4 gap-8 bg-gray-50 rounded-t-lg dark:bg-gray-800 border-b dark:border-b-gray-700 ">
<div className="w-full flex items-center truncate gap-2 text-lg">
<Icon
className="w-10 h-10 p-1 rounded"
style={{
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
}}
/>
<div className="ml-2 truncate">{data.type}</div>
{validationStatus && validationStatus !== "error" ?
<Tooltip title={
<div className="max-h-96 overflow-auto">
{validationStatus}
</div>}>
<ExclamationCircleIcon className="w-5 hover:text-gray-500 hover:dark:text-gray-300" />
</Tooltip>
: <></>
}
</div>
<div className="flex gap-3">
<button
className="relative"
onClick={(event) => {
event.preventDefault();
openPopUp(<NodeModal data={data} />);
}}
>
<div className=" absolute text-red-600 -top-2 -right-1">
{Object.keys(data.node.template).some(
(t) =>
data.node.template[t].advanced &&
data.node.template[t].required
)
? " *"
: ""}
</div>
<Cog6ToothIcon
className={classNames(
Object.keys(data.node.template).some(
(t) =>
data.node.template[t].advanced && data.node.template[t].show
)
? ""
: "hidden",
"w-6 h-6 dark:text-gray-300 hover:animate-spin"
)}
></Cog6ToothIcon>
</button>
<button
onClick={() => {
deleteNode(data.id);
}}
>
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500"></TrashIcon>
</button>
</div>
</div>
<div className="w-full h-full py-5">
<div className="w-full text-gray-500 dark:text-gray-300 px-5 pb-3 text-sm">
{data.node.description}
</div>
<>
{Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_")
.map((t: string, idx) => (
<div key={idx}>
{idx === 0 ? (
{/* {idx === 0 ? (
<div
className={classNames(
"px-5 py-2 mt-2 dark:text-white text-center",
Object.keys(data.node.template).filter(
(key) =>
!key.startsWith("_") && data.node.template[key].show
!key.startsWith("_") &&
data.node.template[key].show &&
!data.node.template[key].advanced
).length === 0
? "hidden"
: ""
@ -86,8 +185,9 @@ export default function GenericNode({
</div>
) : (
<></>
)}
{data.node.template[t].show ? (
)} */}
{data.node.template[t].show &&
!data.node.template[t].advanced ? (
<ParameterComponent
data={data}
color={
@ -98,8 +198,8 @@ export default function GenericNode({
data.node.template[t].display_name
? data.node.template[t].display_name
: data.node.template[t].name
? snakeToNormalCase(data.node.template[t].name)
: snakeToNormalCase(t)
? toNormalCase(data.node.template[t].name)
: toNormalCase(t)
}
name={t}
tooltipTitle={
@ -117,9 +217,17 @@ export default function GenericNode({
)}
</div>
))}
<div className="px-5 py-2 mt-2 dark:text-white text-center">
Output
<div
className={classNames(
Object.keys(data.node.template).length < 1 ? "hidden" : "",
"w-full flex justify-center"
)}
>
{" "}
</div>
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
Output
</div> */}
<ParameterComponent
data={data}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}

View file

@ -1,134 +1,154 @@
import { XCircleIcon, XMarkIcon, InformationCircleIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
import {
XCircleIcon,
XMarkIcon,
InformationCircleIcon,
CheckCircleIcon,
} from "@heroicons/react/24/outline";
import { Link } from "react-router-dom";
import { Transition } from "@headlessui/react";
import { useState } from "react";
import { SingleAlertComponentType } from "../../../../types/alerts";
export default function SingleAlert({ dropItem, removeAlert}:SingleAlertComponentType) {
const [show, setShow] = useState(true);
const type = dropItem.type;
export default function SingleAlert({
dropItem,
removeAlert,
}: SingleAlertComponentType) {
const [show, setShow] = useState(true);
const type = dropItem.type;
return (
<Transition
className="relative"
show={show}
appear={true}
enter="transition-transform duration-500 ease-out"
enterFrom={"transform translate-x-[-100%]"}
enterTo={"transform translate-x-0"}
leave="transition-transform duration-500 ease-in"
leaveFrom={"transform translate-x-0"}
leaveTo={"transform translate-x-[-100%]"}
>
{type === "error"?
<div className="flex bg-red-50 rounded-md p-3 mb-2 mx-2" key={dropItem.id}>
<div className="flex-shrink-0">
<XCircleIcon
className="h-5 w-5 text-red-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
{dropItem.title}
</h3>
{dropItem.list ? (
<div className="mt-2 text-sm text-red-700">
<ul className="list-disc space-y-1 pl-5">
{dropItem.list.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
</div>
) : (
<></>
)}
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
}}
className="inline-flex rounded-md bg-red-50 p-1.5 text-red-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
:(type === "notice" ?
<div className="flex rounded-md bg-blue-50 p-3 mb-2 mx-2" key={dropItem.id}>
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-blue-400"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700">{dropItem.title}</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
{dropItem.link ? (
<Link
to={dropItem.link}
className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600"
>
Details
</Link>
) : (
<></>
)}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
}}
className="inline-flex rounded-md bg-blue-50 p-1.5 text-blue-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
:
<div className="flex bg-green-50 p-3 mb-2 mx-2 rounded-md" key={dropItem.id}>
<div className="flex-shrink-0">
<CheckCircleIcon
className="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">
{dropItem.title}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
}}
className="inline-flex rounded-md bg-green-50 p-1.5 text-green-500"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
)
}
</Transition>
)
}
return (
<Transition
className="relative"
show={show}
appear={true}
enter="transition-transform duration-500 ease-out"
enterFrom={"transform translate-x-[-100%]"}
enterTo={"transform translate-x-0"}
leave="transition-transform duration-500 ease-in"
leaveFrom={"transform translate-x-0"}
leaveTo={"transform translate-x-[-100%]"}
>
{type === "error" ? (
<div
className="flex bg-red-50 dark:bg-red-900 rounded-md p-3 mb-2 mx-2"
key={dropItem.id}
>
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400 dark:text-red-50" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm break-words font-medium text-red-800 dark:text-white/80">
{dropItem.title}
</h3>
{dropItem.list ? (
<div className="mt-2 text-sm text-red-700 dark:text-red-50">
<ul className="list-disc space-y-1 pl-5">
{dropItem.list.map((item, idx) => (
<li className="break-words" key={idx}>{item}</li>
))}
</ul>
</div>
) : (
<></>
)}
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false);
setTimeout(() => {
removeAlert(dropItem.id);
}, 500);
}}
className="inline-flex rounded-md bg-red-50 dark:bg-transparent p-1.5 text-red-500 dark:text-red-50"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
) : type === "notice" ? (
<div
className="flex rounded-md bg-blue-50 dark:bg-blue-900 p-3 mb-2 mx-2"
key={dropItem.id}
>
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-blue-400 dark:text-blue-50"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700 dark:text-white/80">{dropItem.title}</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
{dropItem.link ? (
<Link
to={dropItem.link}
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-blue-600"
>
Details
</Link>
) : (
<></>
)}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false);
setTimeout(() => {
removeAlert(dropItem.id);
}, 500);
}}
className="inline-flex rounded-md bg-blue-50 dark:bg-transparent p-1.5 text-blue-500 dark:text-blue-50"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
) : (
<div
className="flex bg-green-50 dark:bg-green-900 p-3 mb-2 mx-2 rounded-md"
key={dropItem.id}
>
<div className="flex-shrink-0">
<CheckCircleIcon
className="h-5 w-5 text-green-400 dark:text-green-50"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800 dark:bg-white/80">
{dropItem.title}
</p>
</div>
<div className="ml-auto pl-3">
<div className="-mx-1.5 -my-1.5">
<button
type="button"
onClick={() => {
setShow(false);
setTimeout(() => {
removeAlert(dropItem.id);
}, 500);
}}
className="inline-flex rounded-md bg-green-50 dark:bg-transparent p-1.5 text-green-500 dark:text-green-50"
>
<span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
)}
</Transition>
);
}

View file

@ -5,29 +5,15 @@ import { TrashIcon } from "@heroicons/react/24/outline";
import SingleAlert from "./components/singleAlertComponent";
import { AlertDropdownType } from "../../types/alerts";
import { PopUpContext } from "../../contexts/popUpContext";
import { useOnClickOutside } from "../hooks/useOnClickOutside";
export default function AlertDropdown({}: AlertDropdownType) {
const { closePopUp } = useContext(PopUpContext);
const componentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
componentRef.current &&
!componentRef.current.contains(event.target as Node)
) {
closePopUp();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
// Cleanup the event listener when the component is unmounted
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [componentRef]);
// Use the custom hook
useOnClickOutside(componentRef, () => {
closePopUp();
});
const {
notificationList,
@ -38,13 +24,13 @@ export default function AlertDropdown({}: AlertDropdownType) {
return (
<div
ref={componentRef}
className="z-10 py-3 pb-4 rounded-md bg-white ring-1 ring-black ring-opacity-5 shadow-lg focus:outline-none overflow-hidden w-[16rem] h-[28rem] flex flex-col"
className="z-10 py-3 pb-4 px-2 rounded-md bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 shadow-lg focus:outline-none overflow-hidden w-[400px] h-[500px] flex flex-col"
>
<div className="flex pl-3 flex-row justify-between text-md font-medium text-gray-800">
<div className="flex pl-3 flex-row justify-between text-md font-medium text-gray-800 dark:text-gray-200">
Notifications
<div className="flex gap-2 pr-3 ">
<div className="flex gap-3 pr-3 ">
<button
className="hover:text-red-500"
className="text-gray-800 hover:text-red-500 dark:text-gray-200 dark:hover:text-red-500"
onClick={() => {
closePopUp();
setTimeout(clearNotificationList, 100);
@ -52,12 +38,15 @@ export default function AlertDropdown({}: AlertDropdownType) {
>
<TrashIcon className="w-[1.1rem] h-[1.1rem]" />
</button>
<button className="hover:text-red-500" onClick={closePopUp}>
<button
className="text-gray-800 hover:text-red-500 dark:text-gray-200 dark:hover:text-red-500"
onClick={closePopUp}
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="mt-2 flex flex-col overflow-y-scroll w-full h-full scrollbar-hide">
<div className="mt-3 flex flex-col overflow-y-scroll w-full h-full scrollbar-hide text-gray-900 dark:text-gray-300">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
@ -67,7 +56,7 @@ export default function AlertDropdown({}: AlertDropdownType) {
/>
))
) : (
<div className="h-full w-full pb-16 text-gray-500 flex justify-center items-center">
<div className="h-full w-full pb-16 text-gray-500 dark:text-gray-500 flex justify-center items-center">
No new notifications
</div>
)}

View file

@ -39,16 +39,16 @@ export default function ErrorAlert({
removeAlert(id);
}, 500);
}}
className="rounded-md w-96 mt-6 shadow-xl bg-red-50 p-4 cursor-pointer"
className="rounded-md w-96 mt-6 shadow-xl bg-red-50 dark:bg-red-900 p-4 cursor-pointer"
>
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
<XCircleIcon className="h-5 w-5 text-red-400 dark:text-red-50" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">{title}</h3>
<h3 className="text-sm font-medium text-red-800 dark:text-white/80">{title}</h3>
{list.length !== 0 ? (
<div className="mt-2 text-sm text-red-700">
<div className="mt-2 text-sm text-red-700 dark:text-red-50">
<ul className="list-disc space-y-1 pl-5">
{list.map((item, index) => (
<li key={index}>{item}</li>

View file

@ -0,0 +1,33 @@
import { useEffect } from "react";
export function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or its children
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
// Attach the listener to the document
document.addEventListener("mousedown", listener, { passive: true });
// Attach the listener to the react-flow instance
const reactFlowContainer = document.querySelector(".react-flow");
if (reactFlowContainer) {
reactFlowContainer.addEventListener("mousedown", listener, {
passive: true,
});
}
// Clean up the listener when the component is unmounted
return () => {
document.removeEventListener("mousedown", listener);
if (reactFlowContainer) {
reactFlowContainer.removeEventListener("mousedown", listener);
}
};
}, [ref, handler]); // Rerun only if ref or handler changes
}

View file

@ -36,22 +36,22 @@ export default function NoticeAlert({
setShow(false);
removeAlert(id);
}}
className="rounded-md w-96 mt-6 shadow-xl bg-blue-50 p-4"
className="rounded-md w-96 mt-6 shadow-xl bg-blue-50 dark:bg-blue-900 p-4"
>
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-blue-400"
className="h-5 w-5 text-blue-400 dark:text-blue-50"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700">{title}</p>
<p className="text-sm text-blue-700 dark:text-white/80">{title}</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
{link !== "" ? (
<Link
to={link}
className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600"
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-blue-600"
>
Details
</Link>

View file

@ -34,17 +34,17 @@ export default function SuccessAlert({
setShow(false);
removeAlert(id);
}}
className="rounded-md w-96 mt-6 shadow-xl bg-green-50 p-4"
className="rounded-md w-96 mt-6 shadow-xl bg-green-50 dark:bg-green-900 p-4"
>
<div className="flex">
<div className="flex-shrink-0">
<CheckCircleIcon
className="h-5 w-5 text-green-400"
className="h-5 w-5 text-green-400 dark:text-green-50"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">{title}</p>
<p className="text-sm font-medium text-green-800 dark:text-white/80">{title}</p>
</div>
</div>
</div>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="271px" height="271px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<defs>
<filter id="ldio-978hsxudfzl-filter" x="-100%" y="-100%" width="300%" height="300%" color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="3.6"></feGaussianBlur>
<feComponentTransfer result="cutoff">
<feFuncA type="table" tableValues="0 0 0 0 0 0 1 1 1 1 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
<g filter="url(#ldio-978hsxudfzl-filter)"><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#2edbb5">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="5s" repeatCount="indefinite" begin="-0.2s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="5s" repeatCount="indefinite" begin="0s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#1d99ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="2.5s" repeatCount="indefinite" begin="-0.15000000000000002s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="2.5s" repeatCount="indefinite" begin="-0.05s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#4f41ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="1.6666666666666665s" repeatCount="indefinite" begin="-0.1s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="1.6666666666666665s" repeatCount="indefinite" begin="-0.1s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#8400ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="1.25s" repeatCount="indefinite" begin="-0.05s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="1.25s" repeatCount="indefinite" begin="-0.15000000000000002s"></animateTransform>
</g>
</g></g>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -2,12 +2,14 @@ export default function CrashErrorComponent({ error, resetErrorBoundary }) {
return (
<div className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-gray-800 bg-opacity-50 z-50">
<div className="bg-white max-w-4xl h-1/3 min-h-fit rounded-lg shadow-lg p-8 text-start flex flex-col justify-evenly">
<h1 className="text-red-500 text-3xl mb-4">Oops! An unknown error has occurred.</h1>
<h1 className="text-red-500 text-3xl mb-4">
Oops! An unknown error has occurred.
</h1>
<p className="text-gray-700 mb-4 text-xl">
Please click the 'Reset Application' button
to restore the application's state. If the error persists, please
create an issue on our GitHub page. We apologize for any inconvenience
this may have caused.
Please click the 'Reset Application' button to restore the
application's state. If the error persists, please create an issue on
our GitHub page. We apologize for any inconvenience this may have
caused.
</p>
<div className="flex justify-center">
<button

View file

@ -6,119 +6,119 @@ import { classNames } from "../../utils";
import { locationContext } from "../../contexts/locationContext";
export default function ExtraSidebar() {
const {
current,
isStackedOpen,
setIsStackedOpen,
extraNavigation,
extraComponent,
} = useContext(locationContext);
return (
<>
<aside
className={` ${
isStackedOpen ? "w-52" : "w-0 "
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
>
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start">
<div className="flex pt-1 px-4 justify-between align-middle w-full">
<span className="text-gray-900 dark:text-white py-[2px] font-medium ">
{extraNavigation.title}
</span>
</div>
<div className="flex flex-grow flex-col w-full">
{extraNavigation.options ? (
<div className="p-4">
<nav className="flex-1 space-y-1">
{extraNavigation.options.map((item) =>
!item.children ? (
<div key={item.name}>
<Link
to={item.href}
className={classNames(
item.href.split("/")[2] === current[4]
? "bg-gray-100 text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
)}
>
<item.icon
className={classNames(
item.href.split("/")[2] === current[4]
? "text-gray-500"
: "text-gray-400 group-hover:text-gray-500",
"mr-3 flex-shrink-0 h-6 w-6"
)}
/>
{item.name}
</Link>
</div>
) : (
<Disclosure
as="div"
key={item.name}
className="space-y-1"
>
{({ open }) => (
<>
<Disclosure.Button
className={classNames(
item.href.split("/")[2] === current[4]
? "bg-gray-100 text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
)}
>
<item.icon
className="mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span className="flex-1">{item.name}</span>
<svg
className={classNames(
open
? "text-gray-400 rotate-90"
: "text-gray-300",
"ml-3 h-5 w-5 flex-shrink-0 transition-rotate duration-150 ease-in-out group-hover:text-gray-400"
)}
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
d="M6 6L14 10L6 14V6Z"
fill="currentColor"
/>
</svg>
</Disclosure.Button>
<Disclosure.Panel className="space-y-1">
{item.children.map((subItem) => (
<Link
key={subItem.name}
to={subItem.href}
className={classNames(
subItem.href.split("/")[3] === current[5]
? "bg-gray-100 text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
)}
>
{subItem.name}
</Link>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
)
)}
</nav>
</div>
) : (
extraComponent
)}
</div>
</div>
</aside>
</>
);
const {
current,
isStackedOpen,
setIsStackedOpen,
extraNavigation,
extraComponent,
} = useContext(locationContext);
return (
<>
<aside
className={` ${
isStackedOpen ? "w-52" : "w-0 "
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
>
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start">
<div className="flex pt-1 px-4 justify-between align-middle w-full">
<span className="text-gray-900 dark:text-white py-[2px] font-medium ">
{extraNavigation.title}
</span>
</div>
<div className="flex flex-grow flex-col w-full">
{extraNavigation.options ? (
<div className="p-4">
<nav className="flex-1 space-y-1">
{extraNavigation.options.map((item) =>
!item.children ? (
<div key={item.name}>
<Link
to={item.href}
className={classNames(
item.href.split("/")[2] === current[4]
? "bg-gray-100 text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
)}
>
<item.icon
className={classNames(
item.href.split("/")[2] === current[4]
? "text-gray-500"
: "text-gray-400 group-hover:text-gray-500",
"mr-3 flex-shrink-0 h-6 w-6"
)}
/>
{item.name}
</Link>
</div>
) : (
<Disclosure
as="div"
key={item.name}
className="space-y-1"
>
{({ open }) => (
<>
<Disclosure.Button
className={classNames(
item.href.split("/")[2] === current[4]
? "bg-gray-100 text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
)}
>
<item.icon
className="mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span className="flex-1">{item.name}</span>
<svg
className={classNames(
open
? "text-gray-400 rotate-90"
: "text-gray-300",
"ml-3 h-5 w-5 flex-shrink-0 transition-rotate duration-150 ease-in-out group-hover:text-gray-400"
)}
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
d="M6 6L14 10L6 14V6Z"
fill="currentColor"
/>
</svg>
</Disclosure.Button>
<Disclosure.Panel className="space-y-1">
{item.children.map((subItem) => (
<Link
key={subItem.name}
to={subItem.href}
className={classNames(
subItem.href.split("/")[3] === current[5]
? "bg-gray-100 text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
)}
>
{subItem.name}
</Link>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
)
)}
</nav>
</div>
) : (
extraComponent
)}
</div>
</div>
</aside>
</>
);
}

View file

@ -1,17 +1,17 @@
import { styled } from '@mui/material/styles';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import { styled } from "@mui/material/styles";
import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip";
export const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.common.white,
color: 'rgba(0, 0, 0, 0.87)',
boxShadow: theme.shadows[2],
fontSize: 14,
},
[`& .${tooltipClasses.arrow}:before`]: {
color: theme.palette.common.white,
boxShadow: theme.shadows[1],
},
}));
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.common.white,
color: "rgba(0, 0, 0, 0.87)",
boxShadow: theme.shadows[2],
fontSize: 14,
},
[`& .${tooltipClasses.arrow}:before`]: {
color: theme.palette.common.white,
boxShadow: theme.shadows[1],
},
}));

View file

@ -1,6 +1,15 @@
import { ReactElement } from "react";
import { LightTooltip } from "../LightTooltipComponent";
import { TooltipComponentType } from "../../types/components";
export default function Tooltip({ children, title }:{children:ReactElement,title:string}) {
return <LightTooltip title={title} arrow>{children}</LightTooltip>;
export default function Tooltip({
children,
title,
placement,
}: TooltipComponentType) {
return (
<LightTooltip placement={placement} title={title} arrow>
{children}
</LightTooltip>
);
}

View file

@ -1,8 +1,12 @@
import { ChatBubbleLeftEllipsisIcon, ChatBubbleOvalLeftEllipsisIcon, PlusSmallIcon } from "@heroicons/react/24/outline";
import {
ChatBubbleLeftEllipsisIcon,
ChatBubbleOvalLeftEllipsisIcon,
PlusSmallIcon,
} from "@heroicons/react/24/outline";
import { useState } from "react";
import { ChatMessageType } from "../../../types/chat";
import { nodeColors } from "../../../utils";
var Convert = require('ansi-to-html');
import Convert from "ansi-to-html";
var convert = new Convert({newline:true});
export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
@ -29,13 +33,16 @@ export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
style={{ backgroundColor: nodeColors["thought"] }}
className=" text-start inline-block w-full pb-3 pt-3 px-5 cursor-pointer"
dangerouslySetInnerHTML={{
__html: convert.toHtml(chat.thought)
__html: convert.toHtml(chat.thought),
}}
></div>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="w-full rounded-b-md px-4 pb-3 pt-3 pr-8" style={{ backgroundColor: nodeColors["chat"] }}>
{chat.message}
<div
className="w-full rounded-b-md px-4 pb-3 pt-3 pr-8"
style={{ backgroundColor: nodeColors["chat"] }}
>
{chat.message}
</div>
</div>
</div>

View file

@ -0,0 +1,45 @@
import { Transition } from "@headlessui/react";
import {
Bars3CenterLeftIcon,
ChatBubbleBottomCenterTextIcon,
} from "@heroicons/react/24/outline";
import { nodeColors } from "../../../utils";
import { PopUpContext } from "../../../contexts/popUpContext";
import { useContext } from "react";
import ChatModal from "../../../modals/chatModal";
export default function ChatTrigger({ open, setOpen }) {
const { openPopUp } = useContext(PopUpContext);
return (
<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="absolute bottom-2 right-3">
<div
style={{ backgroundColor: nodeColors["chat"] }}
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full dark:bg-gray-800 dark:border-gray-600 dark:text-white"
>
<button
onClick={() => {
setOpen(true);
}}
>
<div className="flex gap-3 items-center">
<ChatBubbleBottomCenterTextIcon
className="h-6 w-6 mt-1"
style={{ color: "white" }}
/>
</div>
</button>
</div>
</div>
</Transition>
);
}

View file

@ -1,291 +1,32 @@
import { Transition } from "@headlessui/react";
import {
Bars3CenterLeftIcon,
LockClosedIcon,
PaperAirplaneIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import {
MouseEventHandler,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { sendAll } from "../../controllers/API";
import { alertContext } from "../../contexts/alertContext";
import { classNames, nodeColors, snakeToNormalCase } from "../../utils";
import { TabsContext } from "../../contexts/tabsContext";
import { ChatType } from "../../types/chat";
import ChatMessage from "./chatMessage";
import { NodeType } from "../../types/flow";
import { useEffect, useRef, useState } from "react";
const _ = require("lodash");
import { ChatMessageType, ChatType } from "../../types/chat";
import ChatTrigger from "./chatTrigger";
import ChatModal from "../../modals/chatModal";
export default function Chat({ flow, reactFlowInstance }: ChatType) {
const { updateFlow, lockChat, setLockChat, flows, tabIndex } =
useContext(TabsContext);
const [saveChat, setSaveChat] = useState(false);
const [open, setOpen] = useState(true);
const [chatValue, setChatValue] = useState("");
const [chatHistory, setChatHistory] = useState(flow.chat);
const { setErrorData, setNoticeData } = useContext(alertContext);
const addChatHistory = (
message: string,
isSend: boolean,
thought?: string
) => {
let tabsChange = false;
setChatHistory((old) => {
let newChat = _.cloneDeep(old);
if (JSON.stringify(flow.chat) !== JSON.stringify(old)) {
tabsChange = true;
return old;
}
if (thought) {
newChat.push({ message, isSend, thought });
} else {
newChat.push({ message, isSend });
}
return newChat;
});
if (tabsChange) {
if (thought) {
updateFlow({
..._.cloneDeep(flow),
chat: [...flow.chat, { isSend, message, thought }],
});
} else {
updateFlow({
..._.cloneDeep(flow),
chat: [...flow.chat, { isSend, message }],
});
}
}
setSaveChat((chat) => !chat);
};
import _ from "lodash";
export default function Chat({ flow }: ChatType) {
const [open, setOpen] = useState(false);
useEffect(() => {
updateFlow({ ..._.cloneDeep(flow), chat: chatHistory });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveChat]);
useEffect(() => {
setChatHistory(flow.chat);
}, [flow]);
useEffect(() => {
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
}, [chatHistory]);
function validateNode(n: NodeType): Array<string> {
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
setNoticeData({
title:
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
});
return [];
}
const {
type,
node: { template },
} = n.data;
return Object.keys(template).reduce(
(errors: Array<string>, t) =>
errors.concat(
(template[t].required && template[t].show) &&
(!template[t].value || template[t].value === "") &&
!reactFlowInstance
.getEdges()
.some(
(e) =>
e.targetHandle.split("|")[1] === t &&
e.targetHandle.split("|")[2] === n.id
)
? [
`${type} is missing ${
template.display_name
? template.display_name
: snakeToNormalCase(template[t].name)
}.`,
]
: []
),
[] as string[]
);
}
function validateNodes() {
return reactFlowInstance
.getNodes()
.flatMap((n: NodeType) => validateNode(n));
}
const ref = useRef(null);
function sendMessage() {
if (chatValue !== "") {
let nodeValidationErrors = validateNodes();
if (nodeValidationErrors.length === 0) {
setLockChat(true);
let message = chatValue;
setChatValue("");
addChatHistory(message, true);
sendAll({
...reactFlowInstance.toObject(),
message,
chatHistory,
name: flow.name,
description: flow.description,
})
.then((r) => {
addChatHistory(r.data.result, false, r.data.thought);
setLockChat(false);
})
.catch((error) => {
setErrorData({
title: error.message ?? "Unknown Error",
list: [error.response.data.detail],
});
setLockChat(false);
let lastMessage;
setChatHistory((chatHistory) => {
let newChat = chatHistory;
lastMessage = newChat.pop().message;
return newChat;
});
setChatValue(lastMessage);
});
} else {
setErrorData({
title: "Oops! Looks like you missed some required information:",
list: nodeValidationErrors,
});
const handleKeyDown = (event: KeyboardEvent) => {
if (
(event.key === "K" || event.key === "k") &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
setOpen((oldState) => !oldState);
}
} else {
setErrorData({
title: "Error sending message",
list: ["The message cannot be empty."],
});
}
}
function clearChat() {
setChatHistory([]);
updateFlow({ ..._.cloneDeep(flow), chat: [] });
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<>
<Transition
show={open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="w-[340px] absolute bottom-0 right-1">
<div className="border dark:border-gray-700 h-full rounded-xl rounded-b-none bg-white dark:bg-gray-800 shadow">
<div
onClick={() => {
setOpen(false);
}}
className="flex justify-between cursor-pointer items-center px-5 py-2 border-b dark:border-b-gray-700"
>
<div className="flex gap-3 text-lg dark:text-white font-medium items-center">
<Bars3CenterLeftIcon
className="h-5 w-5 mt-1"
style={{ color: nodeColors["chat"] }}
/>
Chat
</div>
<button
className="hover:text-blue-500 dark:text-white"
onClick={(e) => {
e.stopPropagation();
clearChat();
}}
>
Clear
</button>
</div>
<div className="w-full h-[400px] flex gap-3 mb-auto overflow-y-auto scrollbar-hide flex-col bg-gray-50 dark:bg-gray-900 p-3 py-5">
{chatHistory.map((c, i) => (
<ChatMessage chat={c} key={i} />
))}
<div ref={ref}></div>
</div>
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex items-center justify-between p-3">
<div className="relative w-full mt-1 rounded-md shadow-sm">
<input
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat) {
sendMessage();
}
}}
type="text"
disabled={lockChat}
value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => {
setChatValue(e.target.value);
}}
className={classNames(
lockChat ? "bg-gray-500 text-white" : "dark:bg-gray-700",
"form-input block w-full rounded-md border-gray-300 dark:border-gray-600 dark:text-white pr-10 sm:text-sm"
)}
placeholder={"Send a message..."}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<LockClosedIcon
className="h-5 w-5 text-gray-400 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true"
/>
) : (
<PaperAirplaneIcon
className="h-5 w-5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true"
/>
)}
</button>
</div>
</div>
</div>
</div>
</div>
</Transition>
<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="absolute bottom-0 right-1">
<div className="border flex justify-center align-center py-1 px-3 rounded-xl rounded-b-none bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-white shadow">
<button
onClick={() => {
setOpen(true);
}}
>
<div className="flex gap-3 items-center">
<Bars3CenterLeftIcon
className="h-6 w-6 mt-1"
style={{ color: nodeColors["chat"] }}
/>
Chat
</div>
</button>
</div>
</div>
</Transition>
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
<ChatTrigger open={open} setOpen={setOpen} />
</>
);
}

View file

@ -19,9 +19,24 @@ export default function CodeAreaComponent({
}
}, [disabled, onChange]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"}>
<div
className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
}
>
<div className="w-full flex items-center gap-3">
<span
onClick={() => {
openPopUp(
<CodeAreaModal
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")

View file

@ -4,80 +4,91 @@ import { Fragment, useState } from "react";
import { DropDownComponentType } from "../../types/components";
import { classNames } from "../../utils";
export default function Dropdown({value, options, onSelect}:DropDownComponentType) {
let [internalValue,setInternalValue] = useState(value===""||!value?"Choose an option":value)
return (
<>
<Listbox value={internalValue} onChange={(value)=>{
setInternalValue(value)
onSelect(value)
}}>
{({ open }) => (
<>
<div className="relative mt-1">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<span className="block w-max truncate">{internalValue}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
export default function Dropdown({
value,
options,
onSelect,
}: DropDownComponentType) {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
);
return (
<>
<Listbox
value={internalValue}
onChange={(value) => {
setInternalValue(value);
onSelect(value);
}}
>
{({ open }) => (
<>
<div className="relative mt-1">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white dark:bg-gray-900 py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<span className="block w-max truncate">{internalValue}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{options.map((option, id) => (
<Listbox.Option
key={id}
className={({ active }) =>
classNames(
active ? "text-white bg-indigo-600" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
)
}
value={option}
>
{({ selected, active }) => (
<>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{option}
</span>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full dark:bg-gray-800 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{options.map((option, id) => (
<Listbox.Option
key={id}
className={({ active }) =>
classNames(
active
? "text-white bg-indigo-600 dark:bg-indigo-500"
: "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9 dark:text-gray-300 dark:bg-gray-800"
)
}
value={option}
>
{({ selected, active }) => (
<>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{option}
</span>
{selected ? (
<span
className={classNames(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon
className="h-5 w-5"
aria-hidden="true"
/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
</>
);
{selected ? (
<span
className={classNames(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon
className="h-5 w-5"
aria-hidden="true"
/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
</>
);
}

View file

@ -1,26 +1,41 @@
import { useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { FloatComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
export default function FloatComponent({value, onChange, disabled}: FloatComponentType){
const [myValue, setMyValue] = useState(value ?? "");
useEffect(()=> {
if(disabled){
setMyValue("");
onChange("");
}
}, [disabled, onChange])
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<input
type="number"
value={myValue}
className={"block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + (disabled ? " bg-gray-200 dark:bg-gray-700" : "")}
placeholder="Type a number from zero to one"
onChange={(e) => {
setMyValue(e.target.value);
onChange(e.target.value);
}}
/>
</div>
);
}
export default function FloatComponent({
value,
onChange,
disabled,
}: FloatComponentType) {
const [myValue, setMyValue] = useState(value ?? "");
useEffect(() => {
if (disabled) {
setMyValue("");
onChange("");
}
}, [disabled, onChange]);
const {setDisableCP} = useContext(TabsContext)
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<input
type="number"
value={myValue}
className={
"block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
}
placeholder="Type a number from zero to one"
onChange={(e) => {
setMyValue(e.target.value);
onChange(e.target.value);
}}
onBlur={() => {
setDisableCP(false)
}}
onFocus={() => {
setDisableCP(true)
}}
/>
</div>
);
}

View file

@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { InputComponentType } from "../../types/components";
import { classNames } from "../../utils";
import { TabsContext } from "../../contexts/tabsContext";
export default function InputComponent({
value,
@ -9,6 +10,8 @@ export default function InputComponent({
password,
}: InputComponentType) {
const [myValue, setMyValue] = useState(value ?? "");
const [pwdVisible, setPwdVisible] = useState(false);
const {setDisableCP} = useContext(TabsContext)
useEffect(() => {
if (disabled) {
setMyValue("");
@ -16,14 +19,25 @@ export default function InputComponent({
}
}, [disabled, onChange]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div
className={
disabled
? "relative pointer-events-none cursor-not-allowed"
: "relative"
}
>
<input
type="text"
value={myValue}
onBlur={() => {
setDisableCP(false)
}}
onFocus={() => {
setDisableCP(true)
}}
className={classNames(
"block w-full form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
"block w-full pr-12 form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
disabled ? " bg-gray-200 dark:bg-gray-700" : "",
password && myValue.length>0?"password":""
password && !pwdVisible && myValue !== "" ? "password" : ""
)}
placeholder="Type a text"
onChange={(e) => {
@ -31,6 +45,50 @@ export default function InputComponent({
onChange(e.target.value);
}}
/>
<button
className="absolute inset-y-0 right-0 items-center px-4 text-gray-600"
onClick={() => {
setPwdVisible(!pwdVisible);
}}
>
{password &&
(pwdVisible ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
))}
</button>
</div>
);
}

View file

@ -9,7 +9,7 @@ export default function InputFileComponent({
disabled,
suffixes,
fileTypes,
onFileChange
onFileChange,
}: FileComponentType) {
const [myValue, setMyValue] = useState(value);
const { setErrorData } = useContext(alertContext);
@ -17,23 +17,23 @@ export default function InputFileComponent({
if (disabled) {
setMyValue("");
onChange("");
onFileChange("")
onFileChange("");
}
}, [disabled, onChange]);
function attachFile(fileReadEvent: ProgressEvent<FileReader>) {
fileReadEvent.preventDefault();
const file = fileReadEvent.target.result;
onFileChange(file as string)
onFileChange(file as string);
}
function checkFileType(fileName:string):boolean{
function checkFileType(fileName: string): boolean {
for (let index = 0; index < suffixes.length; index++) {
if(fileName.endsWith(suffixes[index])){
return true
if (fileName.endsWith(suffixes[index])) {
return true;
}
}
return false
return false;
}
const handleButtonClick = () => {
@ -69,8 +69,9 @@ export default function InputFileComponent({
>
<div className="w-full flex items-center gap-3">
<span
onClick={handleButtonClick}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
"truncate block w-full text-gray-500 dark:text-gray-300 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
>

View file

@ -1,54 +1,84 @@
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { InputListComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
var _ = require("lodash");
import _ from "lodash";
export default function InputListComponent({ value, onChange, disabled}:InputListComponentType) {
const [inputList, setInputList] = useState(value ?? [""]);
useEffect(()=> {
if(disabled){
setInputList([""]);
onChange([""]);
}
}, [disabled, onChange])
return (
<div className={(disabled ? "pointer-events-none cursor-not-allowed" : "") + "flex flex-col gap-3"}>
{inputList.map((i, idx) => (
<div key={idx} className="w-full flex gap-3">
<input
type="text"
value={i}
className={"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + (disabled ? " bg-gray-200" : "")}
placeholder="Type a text"
onChange={(e) => {
setInputList((old) => {
let newInputList = _.cloneDeep(old);
newInputList[idx] = e.target.value;
return newInputList;
});
onChange(inputList);
}}
/>
{idx === inputList.length - 1 ?
<button onClick={() => {setInputList((old) => {
let newInputList = _.cloneDeep(old);
newInputList.push('');
return newInputList;
});
onChange(inputList);}}>
<PlusIcon className="w-4 h-4 hover:text-blue-600" />
</button>
: <button onClick={() => {setInputList((old) => {
let newInputList = _.cloneDeep(old);
newInputList.splice(idx, 1);
return newInputList;
});
onChange(inputList);}}>
<XMarkIcon className="w-4 h-4 hover:text-red-600" />
</button>}
</div>
))}
</div>
);
export default function InputListComponent({
value,
onChange,
disabled,
}: InputListComponentType) {
const [inputList, setInputList] = useState(value ?? [""]);
useEffect(() => {
if (disabled) {
setInputList([""]);
onChange([""]);
}
}, [disabled, onChange]);
const {setDisableCP} = useContext(TabsContext)
return (
<div
className={
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
"flex flex-col gap-3"
}
>
{inputList.map((i, idx) => (
<div key={idx} className="w-full flex gap-3">
<input
type="text"
value={i}
className={
"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
placeholder="Type a text"
onChange={(e) => {
setInputList((old) => {
let newInputList = _.cloneDeep(old);
newInputList[idx] = e.target.value;
return newInputList;
});
onChange(inputList);
}}
onBlur={() => {
setDisableCP(false)
}}
onFocus={() => {
setDisableCP(true)
}}
/>
{idx === inputList.length - 1 ? (
<button
onClick={() => {
setInputList((old) => {
let newInputList = _.cloneDeep(old);
newInputList.push("");
return newInputList;
});
onChange(inputList);
}}
>
<PlusIcon className="w-4 h-4 hover:text-blue-600" />
</button>
) : (
<button
onClick={() => {
setInputList((old) => {
let newInputList = _.cloneDeep(old);
newInputList.splice(idx, 1);
return newInputList;
});
onChange(inputList);
}}
>
<XMarkIcon className="w-4 h-4 hover:text-red-600" />
</button>
)}
</div>
))}
</div>
);
}

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { FloatComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
export default function IntComponent({
value,
@ -13,13 +14,25 @@ export default function IntComponent({
onChange("");
}
}, [disabled, onChange]);
const {setDisableCP} =useContext(TabsContext)
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"}>
<div
className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
}
>
<input
onKeyDown={(event) => {
if (event.key !== 'Backspace' && event.key !== 'Enter' && event.key !== 'Delete' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && !/^[-]?\d*$/.test(event.key)) {
event.preventDefault();
}
if (
event.key !== "Backspace" &&
event.key !== "Enter" &&
event.key !== "Delete" &&
event.key !== "ArrowLeft" &&
event.key !== "ArrowRight" &&
!/^[-]?\d*$/.test(event.key)
) {
event.preventDefault();
}
}}
type="number"
value={myValue}
@ -32,7 +45,14 @@ export default function IntComponent({
setMyValue(e.target.value);
onChange(e.target.value);
}}
onBlur={() => {
setDisableCP(false)
}}
onFocus={() => {
setDisableCP(true)
}}
/>
</div>
);
}

View file

@ -1,16 +1,28 @@
type LoadingComponentProps={
remSize:number
type LoadingComponentProps = {
remSize: number;
};
export default function LoadingComponent({ remSize }: LoadingComponentProps) {
return (
<div role="status" className="w-min m-auto">
<svg
aria-hidden="true"
className={`w-${remSize} h-${remSize} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<br></br>
<span className="animate-pulse text-blue-600 text-lg">Loading...</span>
</div>
);
}
export default function LoadingComponent({remSize}:LoadingComponentProps){
return(
<div role="status" className="w-min m-auto">
<svg aria-hidden="true" className={`w-${remSize} h-${remSize} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`} viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
<span className="animate-pulse text-blue-600 text-lg">Loading...</span>
</div>
)
}

View file

@ -6,30 +6,61 @@ import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components";
import PromptAreaModal from "../../modals/promptModal";
export default function PromptAreaComponent({ value, onChange, disabled }:TextAreaComponentType) {
const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext);
useEffect(() => {
if (disabled) {
setMyValue("");
onChange("");
}
}, [disabled, onChange]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed w-full" : " w-full"}>
<div className="w-full flex items-center gap-3">
<span
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
>
{myValue !== "" ? myValue : 'Text empty'}
</span>
<button onClick={()=>{openPopUp(<PromptAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}}>
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600" />
</button>
</div>
</div>
);
export default function PromptAreaComponent({
value,
onChange,
disabled,
}: TextAreaComponentType) {
const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext);
useEffect(() => {
if (disabled) {
setMyValue("");
onChange("");
}
}, [disabled, onChange]);
return (
<div
className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : " w-full"
}
>
<div className="w-full flex items-center gap-3">
<span
onClick={() => {
openPopUp(
<PromptAreaModal
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
>
{myValue !== "" ? myValue : "Text empty"}
</span>
<button
onClick={() => {
openPopUp(
<PromptAreaModal
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
>
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600" />
</button>
</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import CodeAreaModal from "../../modals/codeAreaModal";
import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components";
@ -17,9 +16,9 @@ export default function TextAreaComponent({ value, onChange, disabled }:TextArea
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div className="w-full flex items-center gap-3">
<span
<span onClick={()=>{openPopUp(<TextAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}}
className={
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
"truncate block w-full text-gray-500 dark:text-gray-100 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "")
}
>

View file

@ -3,73 +3,80 @@ import { classNames } from "../../utils";
import { useEffect } from "react";
import { ToggleComponentType } from "../../types/components";
export default function ToggleComponent({ enabled, setEnabled, disabled }:ToggleComponentType) {
useEffect(()=> {
if(disabled){
setEnabled(false);
}
}, [disabled, setEnabled])
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<Switch
checked={enabled}
onChange={(x:boolean) => {
setEnabled(x);
}}
className={classNames(
enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-600",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
)}
>
<span className="sr-only">Use setting</span>
<span
className={classNames(
enabled ? "translate-x-5" : "translate-x-0",
"pointer-events-none relative inline-block h-5 w-5 transform rounded-full shadow ring-0 transition duration-200 ease-in-out", disabled ? "bg-gray-200 dark:bg-gray-600" : "bg-white dark:bg-gray-800"
)}
>
<span
className={classNames(
enabled
? "opacity-0 ease-out duration-100"
: "opacity-100 ease-in duration-200",
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)}
aria-hidden="true"
>
<svg
className="h-3 w-3 text-gray-400"
fill="none"
viewBox="0 0 12 12"
>
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span
className={classNames(
enabled
? "opacity-100 ease-in duration-200"
: "opacity-0 ease-out duration-100",
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)}
aria-hidden="true"
>
<svg
className="h-3 w-3 text-indigo-600"
fill="currentColor"
viewBox="0 0 12 12"
>
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
</svg>
</span>
</span>
</Switch>
</div>
);
export default function ToggleComponent({
enabled,
setEnabled,
disabled,
}: ToggleComponentType) {
useEffect(() => {
if (disabled) {
setEnabled(false);
}
}, [disabled, setEnabled]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<Switch
checked={enabled}
onChange={(x: boolean) => {
setEnabled(x);
}}
className={classNames(
enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-600",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
)}
>
<span className="sr-only">Use setting</span>
<span
className={classNames(
enabled ? "translate-x-5" : "translate-x-0",
"pointer-events-none relative inline-block h-5 w-5 transform rounded-full shadow ring-0 transition duration-200 ease-in-out",
disabled
? "bg-gray-200 dark:bg-gray-600"
: "bg-white dark:bg-gray-800"
)}
>
<span
className={classNames(
enabled
? "opacity-0 ease-out duration-100"
: "opacity-100 ease-in duration-200",
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)}
aria-hidden="true"
>
<svg
className="h-3 w-3 text-gray-400"
fill="none"
viewBox="0 0 12 12"
>
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span
className={classNames(
enabled
? "opacity-100 ease-in duration-200"
: "opacity-0 ease-out duration-100",
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)}
aria-hidden="true"
>
<svg
className="h-3 w-3 text-indigo-600"
fill="currentColor"
viewBox="0 0 12 12"
>
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
</svg>
</span>
</span>
</Switch>
</div>
);
}

View file

@ -1,7 +1,7 @@
import { createContext, ReactNode, useState } from "react";
import { AlertItemType } from "../types/alerts";
var _ = require("lodash");
import _ from "lodash";
//types for alertContextType
type alertContextType = {
@ -20,13 +20,13 @@ type alertContextType = {
notificationCenter: boolean;
setNotificationCenter: (newState: boolean) => void;
notificationList: Array<AlertItemType>;
pushNotificationList: (Object:AlertItemType) => void;
pushNotificationList: (Object: AlertItemType) => void;
clearNotificationList: () => void;
removeFromNotificationList: (index: string) => void;
};
//initial values to alertContextType
const initialValue:alertContextType = {
const initialValue: alertContextType = {
errorData: { title: "", list: [] },
setErrorData: () => {},
errorOpen: false,
@ -49,7 +49,7 @@ const initialValue:alertContextType = {
export const alertContext = createContext<alertContextType>(initialValue);
export function AlertProvider({ children }:{children:ReactNode}) {
export function AlertProvider({ children }: { children: ReactNode }) {
const [errorData, setErrorDataState] = useState<{
title: string;
list?: Array<string>;
@ -73,68 +73,69 @@ export function AlertProvider({ children }:{children:ReactNode}) {
return newNotificationList;
});
};
/**
* Sets the error data state, opens the error dialog and pushes the new error notification to the notification list
* @param newState An object containing the new error data, including title and optional list of error messages
*/
function setErrorData(newState: { title: string; list?: Array<string> }) {
setErrorDataState(newState);
setErrorOpen(true);
if (newState.title) {
setNotificationCenter(true);
pushNotificationList({
type: "error",
title: newState.title,
list: newState.list,
id: _.uniqueId(),
});
/**
* Sets the error data state, opens the error dialog and pushes the new error notification to the notification list
* @param newState An object containing the new error data, including title and optional list of error messages
*/
function setErrorData(newState: { title: string; list?: Array<string> }) {
setErrorDataState(newState);
setErrorOpen(true);
if (newState.title) {
setNotificationCenter(true);
pushNotificationList({
type: "error",
title: newState.title,
list: newState.list,
id: _.uniqueId(),
});
}
}
}
/**
* Sets the state of the notice data and opens the notice modal, also adds a new notice to the notification center if the title is defined.
* @param newState An object containing the title of the notice and optionally a link.
*/
function setNoticeData(newState: { title: string; link?: string }) {
setNoticeDataState(newState);
setNoticeOpen(true);
if (newState.title) {
// Add new notice to notification center
setNotificationCenter(true);
pushNotificationList({
type: "notice",
title: newState.title,
link: newState.link,
id: _.uniqueId(),
});
/**
* Sets the state of the notice data and opens the notice modal, also adds a new notice to the notification center if the title is defined.
* @param newState An object containing the title of the notice and optionally a link.
*/
function setNoticeData(newState: { title: string; link?: string }) {
setNoticeDataState(newState);
setNoticeOpen(true);
if (newState.title) {
// Add new notice to notification center
setNotificationCenter(true);
pushNotificationList({
type: "notice",
title: newState.title,
link: newState.link,
id: _.uniqueId(),
});
}
}
}
/**
* Update the success data state and show a success alert notification.
* @param newState - A state object with a "title" property to set in the success data state.
*/
function setSuccessData(newState: { title: string }) {
setSuccessDataState(newState); // update the success data state with the provided new state
setSuccessOpen(true); // open the success alert
/**
* Update the success data state and show a success alert notification.
* @param newState - A state object with a "title" property to set in the success data state.
*/
function setSuccessData(newState: { title: string }) {
setSuccessDataState(newState); // update the success data state with the provided new state
setSuccessOpen(true); // open the success alert
// If the new state has a "title" property, add a new success notification to the list
if (newState.title) {
setNotificationCenter(true); // show the notification center
pushNotificationList({ // add the new notification to the list
type: "success",
title: newState.title,
id: _.uniqueId(),
});
// If the new state has a "title" property, add a new success notification to the list
if (newState.title) {
setNotificationCenter(true); // show the notification center
pushNotificationList({
// add the new notification to the list
type: "success",
title: newState.title,
id: _.uniqueId(),
});
}
}
}
function clearNotificationList() {
setNotificationList([]);
}
function removeFromNotificationList(index: string) {
// set the notification list to a new array that filters out the alert with the matching id
setNotificationList((prevAlertsList) =>
prevAlertsList.filter((alert) => alert.id !== index)
prevAlertsList.filter((alert) => alert.id !== index)
);
}
}
return (
<alertContext.Provider
value={{

View file

@ -14,13 +14,13 @@ export const darkContext = createContext<darkContextType>(initialValue);
export function DarkProvider({ children }) {
const [dark, setDark] = useState(false);
useEffect(()=>{
if(dark){
useEffect(() => {
if (dark) {
document.getElementById("body").classList.add("dark");
} else {
document.getElementById("body").classList.remove("dark");
}
}, [dark])
}, [dark]);
return (
<darkContext.Provider
value={{
@ -31,4 +31,4 @@ export function DarkProvider({ children }) {
{children}
</darkContext.Provider>
);
}
}

View file

@ -11,17 +11,15 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
return (
<>
<DarkProvider>
<LocationProvider>
<AlertProvider>
<TabsProvider>
<PopUpProvider>
<TypesProvider>
{children}
</TypesProvider>
</PopUpProvider>
</TabsProvider>
</AlertProvider>
</LocationProvider>
<TypesProvider>
<LocationProvider>
<AlertProvider>
<TabsProvider>
<PopUpProvider>{children}</PopUpProvider>
</TabsProvider>
</AlertProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</>
);

View file

@ -32,7 +32,7 @@ type locationContextType = {
//initial value for location context
const initialValue = {
//actual
//actual
current: window.location.pathname.replace(/\/$/g, "").split("/"),
isStackedOpen:
window.innerWidth > 1024 && window.location.pathname.split("/")[1]
@ -50,7 +50,7 @@ const initialValue = {
export const locationContext = createContext<locationContextType>(initialValue);
export function LocationProvider({ children }:{children:ReactNode}) {
export function LocationProvider({ children }: { children: ReactNode }) {
const [current, setCurrent] = useState(initialValue.current);
const [isStackedOpen, setIsStackedOpen] = useState(
initialValue.isStackedOpen

View file

@ -1,9 +1,9 @@
import { createContext } from "react";
import React, { useState } from "react";
//context to set JSX element on the DOM
// context to set JSX element on the DOM
export const PopUpContext = createContext({
openPopUp: (popUpElement: JSX.Element) => {},
openPopUp: (popUpElement: JSX.Element) => {},
closePopUp: () => {},
});
@ -12,20 +12,20 @@ interface PopUpProviderProps {
}
const PopUpProvider = ({ children }: PopUpProviderProps) => {
const [popUpElement, setPopUpElement] = useState<JSX.Element | null>(null);
const [popUpElements, setPopUpElements] = useState<JSX.Element[]>([]);
const openPopUp = (element: JSX.Element) => {
setPopUpElement(element);
setPopUpElements((prevPopUps) => [element, ...prevPopUps]);
};
const closePopUp = () => {
setPopUpElement(null);
setPopUpElements((prevPopUps) => prevPopUps.slice(1));
};
return (
<PopUpContext.Provider value={{ openPopUp, closePopUp }}>
{children}
{popUpElement}
{popUpElements[0]}
</PopUpContext.Provider>
);
};

View file

@ -1,11 +1,21 @@
import { createContext, useEffect, useState, useRef, ReactNode, useContext } from "react";
import {
createContext,
useEffect,
useState,
useRef,
ReactNode,
useContext,
} from "react";
import { FlowType } from "../types/flow";
import { TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase } from "../utils";
import { LangFlowState, TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils";
import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid";
const TabsContextInitialValue: TabsContextType = {
save:()=>{},
save: () => {},
tabIndex: 0,
setTabIndex: (index: number) => {},
flows: [],
@ -13,11 +23,11 @@ const TabsContextInitialValue: TabsContextType = {
addFlow: (flowData?: any) => {},
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => 0,
downloadFlow: (flow:FlowType) => {},
downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {},
lockChat: false,
setLockChat:(prevState:boolean)=>{},
hardReset:()=>{},
hardReset: () => {},
disableCP:false,
setDisableCP:(state:boolean)=>{},
};
export const TabsContext = createContext<TabsContextType>(
@ -25,51 +35,64 @@ export const TabsContext = createContext<TabsContextType>(
);
export function TabsProvider({ children }: { children: ReactNode }) {
const {setNoticeData} = useContext(alertContext)
const { setNoticeData } = useContext(alertContext);
const [tabIndex, setTabIndex] = useState(0);
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(0);
const [lockChat, setLockChat] = useState(false);
const [id, setId] = useState("");
const { templates } = useContext(typesContext);
const newNodeId = useRef(0);
function incrementNodeId() {
newNodeId.current = newNodeId.current + 1;
return newNodeId.current;
}
function save(){
function save() {
if (flows.length !== 0)
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
);
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
);
}
useEffect(() => {
//save tabs locally
save()
save();
}, [flows, id, tabIndex, newNodeId]);
useEffect(() => {
//get tabs locally saved
let cookie = window.localStorage.getItem("tabsData");
if (cookie) {
let cookieObject = JSON.parse(cookie);
if (cookie && Object.keys(templates).length > 0) {
let cookieObject: LangFlowState = JSON.parse(cookie);
cookieObject.flows.forEach((flow) => {
flow.data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length > 0) {
node.data.node.template = updateTemplate(
templates[node.data.type][
"template"
] as unknown as APITemplateType,
node.data.node.template as APITemplateType
);
}
});
});
setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows);
setId(cookieObject.id);
newNodeId.current = cookieObject.nodeId;
}
}, []);
function hardReset(){
newNodeId.current=0;
setTabIndex(0);setFlows([]);setId(0);
}, [templates]);
function hardReset() {
newNodeId.current = 0;
setTabIndex(0);
setFlows([]);
setId("");
}
/**
* Downloads the current flow as a JSON file
*/
function downloadFlow(flow:FlowType) {
function downloadFlow(flow: FlowType) {
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flow)
@ -78,11 +101,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// create a link element and set its properties
const link = document.createElement("a");
link.href = jsonString;
link.download = `${normalCaseToSnakeCase(flows[tabIndex].name)}.json`;
link.download = `${flows[tabIndex].name}.json`;
// simulate a click on the link element to trigger the download
link.click();
setNoticeData({title:"Warning: Critical data,JSON file may including API keys."})
setNoticeData({
title: "Warning: Critical data,JSON file may including API keys.",
});
}
/**
@ -103,7 +128,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// read the file as text
file.text().then((text) => {
// parse the text into a JSON object
addFlow(JSON.parse(text));
let flow: FlowType = JSON.parse(text);
addFlow(flow);
});
}
};
@ -139,19 +166,28 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null;
const description = flow?.description?flow.description:""
const description = flow?.description ? flow.description : "";
if (data) {
data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length > 0) {
node.data.node.template = updateTemplate(
templates[node.data.type]["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType
);
}
});
}
// Create a new flow with a default name if no flow is provided.
let newFlow: FlowType = {
description,
name: "New Flow",
name: flow?.name ?? "New Flow",
id: id.toString(),
data,
chat: flow ? flow.chat : [],
};
// Increment the ID counter.
setId((old) => old + 1);
setId(uuidv4());
// Add the new flow to the list of flows.
setFlows((prevState) => {
@ -171,22 +207,22 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
if (index !== -1) {
newFlows[index].description = newFlow.description??""
newFlows[index].description = newFlow.description ?? "";
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
newFlows[index].chat = newFlow.chat;
}
return newFlows;
});
}
const [disableCP, setDisableCP] = useState(false);
return (
<TabsContext.Provider
value={{
disableCP,
setDisableCP,
save,
hardReset,
lockChat,
setLockChat,
tabIndex,
setTabIndex,
flows,

View file

@ -1,27 +1,73 @@
import { createContext, ReactNode, useState } from "react";
import { Node} from "reactflow";
import { createContext, ReactNode, useEffect, useState } from "react";
import { Node } from "reactflow";
import { typesContextType } from "../types/typesContext";
import { getAll } from "../controllers/API";
import { APIKindType } from "../types/api";
//context to share types adn functions from nodes to flow
const initialValue:typesContextType = {
const initialValue: typesContextType = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {},
types: {},
setTypes: () => {},
templates: {},
setTemplates: () => {},
data: {},
setData: () => {},
};
export const typesContext = createContext<typesContextType>(initialValue);
export function TypesProvider({ children }:{children:ReactNode}) {
export function TypesProvider({ children }: { children: ReactNode }) {
const [types, setTypes] = useState({});
const [reactFlowInstance, setReactFlowInstance] = useState(null);
function deleteNode(idx:string) {
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
useEffect(() => {
async function getTypes(): Promise<void> {
// Make an asynchronous API call to retrieve all data.
let result = await getAll();
// Update the state of the component with the retrieved data.
setData(result.data);
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = result.data[curr][c];
});
return acc;
}, {})
);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Call the getTypes function.
getTypes();
}, [setTypes]);
function deleteNode(idx: string) {
reactFlowInstance.setNodes(
reactFlowInstance.getNodes().filter((n:Node) => n.id !== idx)
reactFlowInstance.getNodes().filter((n: Node) => n.id !== idx)
);
reactFlowInstance.setEdges(
reactFlowInstance
.getEdges()
.filter((ns) => ns.source !== idx && ns.target !== idx)
);
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== idx && ns.target !== idx));
}
return (
<typesContext.Provider
@ -31,6 +77,10 @@ export function TypesProvider({ children }:{children:ReactNode}) {
reactFlowInstance,
setReactFlowInstance,
deleteNode,
setTemplates,
templates,
data,
setData,
}}
>
{children}

View file

@ -1,21 +1,43 @@
import { PromptTypeAPI, errorsTypeAPI } from './../../types/api/index';
import { APIObjectType, sendAllProps } from '../../types/api/index';
import { PromptTypeAPI, errorsTypeAPI } from "./../../types/api/index";
import { APIObjectType, sendAllProps } from "../../types/api/index";
import axios, { AxiosResponse } from "axios";
import { FlowType } from "../../types/flow";
export async function getAll():Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/all`);
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/all`);
}
export async function sendAll(data:sendAllProps) {
return await axios.post(`/predict`, data);
export async function sendAll(data: sendAllProps) {
return await axios.post(`/predict`, data);
}
export async function checkCode(code:string):Promise<AxiosResponse<errorsTypeAPI>>{
return await axios.post('/validate/code',{code})
export async function checkCode(
code: string
): Promise<AxiosResponse<errorsTypeAPI>> {
return await axios.post("/validate/code", { code });
}
export async function checkPrompt(template:string):Promise<AxiosResponse<PromptTypeAPI>>{
export async function checkPrompt(
template: string
): Promise<AxiosResponse<PromptTypeAPI>> {
return await axios.post("/validate/prompt", { template });
}
return await axios.post('/validate/prompt',{template})
}
export async function getExamples(): Promise<FlowType[]> {
const url =
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples";
const response = await axios.get(url);
const jsonFiles = response.data.filter((file: any) => {
return file.name.endsWith(".json");
});
const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await axios.get(file.download_url);
return contentResponse.data;
});
const contents = await Promise.all(contentsPromises);
return contents;
}

View file

@ -1,15 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View file

@ -5,14 +5,15 @@ import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import ContextWrapper from "./contexts";
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<ContextWrapper>
<BrowserRouter>
<App />
<App />
</BrowserRouter>
</ContextWrapper>
);

View file

@ -0,0 +1,186 @@
import { Dialog, Transition } from "@headlessui/react";
import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react';
import { XMarkIcon, CommandLineIcon, CodeBracketSquareIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
import "ace-builds/src-noconflict/ext-language_tools";
// import "ace-builds/webpack-resolver";
import { darkContext } from "../../contexts/darkContext";
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export default function ApiModal({ flowName }) {
const [open, setOpen] = useState(true);
const { dark } = useContext(darkContext);
const { closePopUp } = useContext(PopUpContext);
const [activeTab, setActiveTab] = useState(0);
const ref = useRef();
const [isCopied, setIsCopied] = useState<Boolean>(false);
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
navigator.clipboard.writeText(tabs[activeTab].code).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
});
};
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
const pythonApiCode = `import requests
API_URL = "${window.location.protocol}//${window.location.host}/predict"
def predict(message):
with open("${flowName}.json", "r") as f:
json_data = json.load(f)
payload = {'exported_flow': json_data, 'message': message}
response = requests.post(API_URL, json=payload)
return response.json() # JSON {"result": "Response"}
print(predict("Your message"))`;
const pythonCode = `from langflow import load_flow_from_json
flow = load_flow_from_json("${flowName}.json")
# Now you can use it like any chain
flow("Hey, have you heard of LangFlow?")`;
const tabs = [
{
name: "Python API",
mode: "python",
image: "https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
code: pythonApiCode,
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: pythonCode,
},
]
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500"
onClick={() => {
setModalOpen(false);
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="h-full w-full flex flex-col justify-center items-center">
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
<CodeBracketSquareIcon
className="h-6 w-6 text-blue-600"
aria-hidden="true"
/>
</div>
<div className="mt-4 text-center sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
>
Code
</Dialog.Title>
</div>
</div>
<div className="h-full w-full bg-gray-200 overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
<div className="flex flex-col h-full w-full ">
<div className="flex px-5 z-10">
{tabs.map((tab, index) => (
<button onClick={() => {
setActiveTab(index);
}} className={"p-2 rounded-t-lg w-44 border border-b-0 border-gray-300 dark:border-gray-700 dark:text-gray-300 -mr-px flex justify-center items-center gap-4 " + (activeTab === index ? " bg-white dark:bg-gray-800" : "bg-gray-100 dark:bg-gray-900")}>
{tab.name}
<img src={tab.image} className="w-6" />
</button>
))}
</div>
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full h-full rounded-lg shadow bg-white dark:bg-gray-800">
<div className="w-full flex items-center justify-between mb-2">
<span className="text-sm text-gray-500 dark:text-gray-300">
Export your flow to use it with this code.
</span>
<button
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={copyToClipboard}
>
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
{isCopied ? 'Copied!' : 'Copy code'}
</button>
</div>
<SyntaxHighlighter
className="h-[370px]"
language={tabs[activeTab].mode}
style={oneDark}
customStyle={{ margin: 0 }}
>
{tabs[activeTab].code}
</SyntaxHighlighter>
</div>
</div>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -0,0 +1,166 @@
import { useContext, useState } from "react";
import { TabsContext } from "../../../../contexts/tabsContext";
import InputListComponent from "../../../../components/inputListComponent";
import Dropdown from "../../../../components/dropdownComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import InputComponent from "../../../../components/inputComponent";
import ToggleComponent from "../../../../components/toggleComponent";
import FloatComponent from "../../../../components/floatComponent";
import IntComponent from "../../../../components/intComponent";
import InputFileComponent from "../../../../components/inputFileComponent";
import PromptAreaComponent from "../../../../components/promptComponent";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import { classNames } from "../../../../utils";
export default function ModalField({ data, title, required, id, name, type }) {
const { save } = useContext(TabsContext);
const [enabled, setEnabled] = useState(
data.node.template[name]?.value ?? false
);
const display =
type === "str" ||
type === "int" ||
type === "prompt" ||
type === "bool" ||
type === "float" ||
type === "file" ||
type === "code";
return (
<div
className={classNames(
"flex flex-row w-full items-center justify-between",
display ? "" : "hidden"
)}
>
{display && (
<div>
<span className="mx-2 dark:text-gray-300">{title}</span>
<span className="text-red-600">{required ? " *" : ""}</span>
</div>
)}
{type === "str" && !data.node.template[name].options ? (
<div className="w-1/2">
{data.node.template[name].list ? (
<InputListComponent
disabled={false}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
onChange={(t: string[]) => {
data.node.template[name].value = t;
save();
}}
/>
) : data.node.template[name].multiline ? (
<TextAreaComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
save();
}}
/>
) : (
<InputComponent
disabled={false}
password={data.node.template[name].password ?? false}
value={data.node.template[name].value ?? ""}
onChange={(t) => {
data.node.template[name].value = t;
save();
}}
/>
)}
</div>
) : type === "bool" ? (
<div className="ml-auto">
{" "}
<ToggleComponent
disabled={false}
enabled={enabled}
setEnabled={(t) => {
data.node.template[name].value = t;
setEnabled(t);
save();
}}
/>
</div>
) : type === "float" ? (
<div className="w-1/2">
<FloatComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : type === "str" && data.node.template[name].options ? (
<div className="w-1/2">
<Dropdown
options={data.node.template[name].options}
onSelect={(newValue) => (data.node.template[name].value = newValue)}
value={data.node.template[name].value ?? "Choose an option"}
></Dropdown>
</div>
) : type === "int" ? (
<div className="w-1/2">
<IntComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : type === "file" ? (
<div className="w-1/2">
<InputFileComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
}}
fileTypes={data.node.template[name].fileTypes}
suffixes={data.node.template[name].suffixes}
onFileChange={(t: string) => {
data.node.template[name].content = t;
save();
}}
></InputFileComponent>
</div>
) : type === "prompt" ? (
<div className="w-1/2">
<PromptAreaComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : type === "code" ? (
<div className="w-1/2">
<CodeAreaComponent
disabled={false}
value={data.node.template[name].value ?? ""}
onChange={(t: string) => {
data.node.template[name].value = t;
save();
}}
/>
</div>
) : (
<div className="hidden"></div>
)}
</div>
);
}

View file

@ -0,0 +1,145 @@
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { NodeDataType } from "../../types/flow";
import { nodeColors, nodeIcons, toNormalCase } from "../../utils";
import { typesContext } from "../../contexts/typesContext";
import ModalField from "./components/ModalField";
export default function NodeModal({ data }: { data: NodeDataType }) {
const [open, setOpen] = useState(true);
const { closePopUp } = useContext(PopUpContext);
const { types } = useContext(typesContext);
const ref = useRef();
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
const Icon = nodeIcons[types[data.type]];
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500"
onClick={() => {
setModalOpen(false);
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="h-full w-full flex flex-col justify-center items-center">
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
<Icon
className="w-10 mt-4 h-10 p-1 rounded"
style={{
color:
nodeColors[types[data.type]] ?? nodeColors.unknown,
}}
/>
<div className="mt-4 text-center sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
>
{data.type}
</Dialog.Title>
</div>
</div>
<div className="h-full w-full bg-gray-200 dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
<div className="flex h-full w-full">
<div className="overflow-hidden px-4 sm:p-4 w-full rounded-lg bg-white dark:bg-gray-800 shadow">
<div className="flex flex-col h-full gap-5">
{Object.keys(data.node.template)
.filter(
(t) =>
t.charAt(0) !== "_" &&
data.node.template[t].advanced &&
data.node.template[t].show
)
.map((t: string, idx) => {
return (
<ModalField
key={idx}
data={data}
title={
data.node.template[t].display_name
? data.node.template[t].display_name
: data.node.template[t].name
? toNormalCase(data.node.template[t].name)
: toNormalCase(t)
}
required={data.node.template[t].required}
id={
data.node.template[t].type +
"|" +
t +
"|" +
data.id
}
name={t}
type={data.node.template[t].type}
/>
);
})}
</div>
</div>
</div>
</div>
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
setModalOpen(false);
}}
>
Done
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

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