Merge branch 'logspace-ai:dev' into dev
This commit is contained in:
commit
de5c4b71be
144 changed files with 9981 additions and 17766 deletions
15
.devcontainer/demo/README.md
Normal file
15
.devcontainer/demo/README.md
Normal 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.
|
||||
32
.devcontainer/demo/devcontainer.json
Normal file
32
.devcontainer/demo/devcontainer.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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": {},
|
||||
|
|
|
|||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -30,4 +30,4 @@ jobs:
|
|||
run: poetry install
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
make test
|
||||
make tests
|
||||
|
|
|
|||
18
Makefile
18
Makefile
|
|
@ -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 '----'
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -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.
|
||||
|
||||
[](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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "reactFlow",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
2061
poetry.lock
generated
2061
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
32
src/backend/langflow/api/callback.py
Normal file
32
src/backend/langflow/api/callback.py
Normal 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)
|
||||
26
src/backend/langflow/api/chat.py
Normal file
26
src/backend/langflow/api/chat.py
Normal 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))
|
||||
221
src/backend/langflow/api/chat_manager.py
Normal file
221
src/backend/langflow/api/chat_manager.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
70
src/backend/langflow/api/schemas.py
Normal file
70
src/backend/langflow/api/schemas.py
Normal 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
|
||||
|
|
@ -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})
|
||||
|
|
|
|||
1
src/backend/langflow/cache/__init__.py
vendored
1
src/backend/langflow/cache/__init__.py
vendored
|
|
@ -0,0 +1 @@
|
|||
from langflow.cache.manager import cache_manager # noqa
|
||||
|
|
@ -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
150
src/backend/langflow/cache/manager.py
vendored
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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}()"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
0
src/backend/langflow/interface/utilities/__init__.py
Normal file
0
src/backend/langflow/interface/utilities/__init__.py
Normal file
39
src/backend/langflow/interface/utilities/base.py
Normal file
39
src/backend/langflow/interface/utilities/base.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
FROM node:14-alpine
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
16583
src/frontend/package-lock.json
generated
16583
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
src/frontend/postcss.config.js
Normal file
6
src/frontend/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -7,4 +7,4 @@ packagejson=$(cat package.json)
|
|||
|
||||
packagejson=$(echo "$packagejson" | jq ".proxy = \"$backend_url\"")
|
||||
|
||||
echo "$packagejson" > package.json
|
||||
echo "$packagejson" > package.json
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -198,7 +198,9 @@ export default function ParameterComponent({
|
|||
save();
|
||||
}}
|
||||
/>
|
||||
):(<></>)}
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
33
src/frontend/src/alerts/hooks/useOnClickOutside/index.ts
Normal file
33
src/frontend/src/alerts/hooks/useOnClickOutside/index.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
40
src/frontend/src/assets/Gooey Ring-5s-271px.svg
Normal file
40
src/frontend/src/assets/Gooey Ring-5s-271px.svg
Normal 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 |
BIN
src/frontend/src/assets/froze-flow.png
Normal file
BIN
src/frontend/src/assets/froze-flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" : "")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" : "")
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" : "")
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
186
src/frontend/src/modals/ApiModal/index.tsx
Normal file
186
src/frontend/src/modals/ApiModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
145
src/frontend/src/modals/NodeModal/index.tsx
Normal file
145
src/frontend/src/modals/NodeModal/index.tsx
Normal 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
Loading…
Add table
Add a link
Reference in a new issue