Adding API improvements, HomePage and validation improvements (#493)
This commit is contained in:
commit
0f59382704
132 changed files with 16319 additions and 11981 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -241,3 +241,4 @@ dmypy.json
|
||||||
|
|
||||||
# Poetry
|
# Poetry
|
||||||
.testenv/*
|
.testenv/*
|
||||||
|
langflow.db
|
||||||
|
|
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -35,7 +35,7 @@
|
||||||
"name": "Debug Frontend",
|
"name": "Debug Frontend",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"url": "http://localhost:3000/*",
|
"url": "http://localhost:3000/",
|
||||||
"webRoot": "${workspaceRoot}/src/frontend"
|
"webRoot": "${workspaceRoot}/src/frontend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
629
poetry.lock
generated
629
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -29,10 +29,9 @@ google-search-results = "^2.4.1"
|
||||||
google-api-python-client = "^2.79.0"
|
google-api-python-client = "^2.79.0"
|
||||||
typer = "^0.7.0"
|
typer = "^0.7.0"
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
langchain = "^0.0.194"
|
langchain = "^0.0.200"
|
||||||
openai = "^0.27.7"
|
openai = "^0.27.8"
|
||||||
types-pyyaml = "^6.0.12.8"
|
types-pyyaml = "^6.0.12.8"
|
||||||
dill = "^0.3.6"
|
|
||||||
pandas = "^1.5.3"
|
pandas = "^1.5.3"
|
||||||
chromadb = "^0.3.21"
|
chromadb = "^0.3.21"
|
||||||
huggingface-hub = "^0.13.3"
|
huggingface-hub = "^0.13.3"
|
||||||
|
|
@ -57,9 +56,13 @@ jina = "3.15.2"
|
||||||
sentence-transformers = "^2.2.2"
|
sentence-transformers = "^2.2.2"
|
||||||
ctransformers = "^0.2.2"
|
ctransformers = "^0.2.2"
|
||||||
cohere = "^4.6.0"
|
cohere = "^4.6.0"
|
||||||
|
sqlmodel = "^0.0.8"
|
||||||
faiss-cpu = "^1.7.4"
|
faiss-cpu = "^1.7.4"
|
||||||
anthropic = "^0.2.9"
|
anthropic = "^0.2.9"
|
||||||
|
orjson = "^3.9.0"
|
||||||
multiprocess = "^0.70.14"
|
multiprocess = "^0.70.14"
|
||||||
|
cachetools = "^5.3.1"
|
||||||
|
types-cachetools = "^5.3.0.5"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from fastapi import FastAPI
|
||||||
import httpx
|
import httpx
|
||||||
from multiprocess import Process, cpu_count # type: ignore
|
from multiprocess import Process, cpu_count # type: ignore
|
||||||
import platform
|
import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
import socket
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich import box
|
from rich import box
|
||||||
from rich import print as rprint
|
from rich import print as rprint
|
||||||
import typer
|
import typer
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from langflow.main import create_app
|
from langflow.main import create_app
|
||||||
from langflow.settings import settings
|
from langflow.settings import settings
|
||||||
from langflow.utils.logger import configure, logger
|
from langflow.utils.logger import configure, logger
|
||||||
|
|
@ -26,10 +28,19 @@ def get_number_of_workers(workers=None):
|
||||||
return workers
|
return workers
|
||||||
|
|
||||||
|
|
||||||
def update_settings(config: str, dev: bool = False):
|
def update_settings(
|
||||||
|
config: str,
|
||||||
|
dev: bool = False,
|
||||||
|
database_url: Optional[str] = None,
|
||||||
|
remove_api_keys: bool = False,
|
||||||
|
):
|
||||||
"""Update the settings from a config file."""
|
"""Update the settings from a config file."""
|
||||||
if config:
|
if config:
|
||||||
settings.update_from_yaml(config, dev=dev)
|
settings.update_from_yaml(config, dev=dev)
|
||||||
|
if database_url:
|
||||||
|
settings.update_settings(database_url=database_url)
|
||||||
|
if remove_api_keys:
|
||||||
|
settings.update_settings(remove_api_keys=remove_api_keys)
|
||||||
|
|
||||||
|
|
||||||
def serve_on_jcloud():
|
def serve_on_jcloud():
|
||||||
|
|
@ -93,6 +104,10 @@ def serve(
|
||||||
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
|
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
|
||||||
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
|
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
|
||||||
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
||||||
|
database_url: str = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Database URL to connect to. If not provided, a local SQLite database will be used.",
|
||||||
|
),
|
||||||
path: str = typer.Option(
|
path: str = typer.Option(
|
||||||
None,
|
None,
|
||||||
help="Path to the frontend directory containing build files. This is for development purposes only.",
|
help="Path to the frontend directory containing build files. This is for development purposes only.",
|
||||||
|
|
@ -100,23 +115,12 @@ def serve(
|
||||||
open_browser: bool = typer.Option(
|
open_browser: bool = typer.Option(
|
||||||
True, help="Open the browser after starting the server."
|
True, help="Open the browser after starting the server."
|
||||||
),
|
),
|
||||||
|
remove_api_keys: bool = typer.Option(
|
||||||
|
False, help="Remove API keys from the projects saved in the database."
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Run the Langflow server.
|
Run the Langflow server.
|
||||||
|
|
||||||
Args:
|
|
||||||
host (str): Host to bind the server to.
|
|
||||||
workers (int): Number of worker processes.
|
|
||||||
timeout (int): Worker timeout in seconds.
|
|
||||||
port (int): Port to listen on.
|
|
||||||
config (str): Path to the configuration file.
|
|
||||||
env_file (Path): Path to the .env file containing environment variables.
|
|
||||||
log_level (str): Logging level.
|
|
||||||
log_file (Path): Path to the log file.
|
|
||||||
jcloud (bool): Deploy on Jina AI Cloud.
|
|
||||||
dev (bool): Run in development mode (may contain bugs).
|
|
||||||
path (str): Path to the frontend directory containing build files. This is for development purposes only.
|
|
||||||
open_browser (bool): Open the browser after starting the server.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if jcloud:
|
if jcloud:
|
||||||
|
|
@ -125,19 +129,22 @@ def serve(
|
||||||
load_dotenv(env_file)
|
load_dotenv(env_file)
|
||||||
|
|
||||||
configure(log_level=log_level, log_file=log_file)
|
configure(log_level=log_level, log_file=log_file)
|
||||||
update_settings(config, dev=dev)
|
update_settings(
|
||||||
app = create_app()
|
config, dev=dev, database_url=database_url, remove_api_keys=remove_api_keys
|
||||||
|
)
|
||||||
# get the directory of the current file
|
# get the directory of the current file
|
||||||
if not path:
|
if not path:
|
||||||
frontend_path = Path(__file__).parent
|
frontend_path = Path(__file__).parent
|
||||||
static_files_dir = frontend_path / "frontend"
|
static_files_dir = frontend_path / "frontend"
|
||||||
else:
|
else:
|
||||||
static_files_dir = Path(path)
|
static_files_dir = Path(path)
|
||||||
app.mount(
|
|
||||||
"/",
|
app = create_app()
|
||||||
StaticFiles(directory=static_files_dir, html=True),
|
setup_static_files(app, static_files_dir)
|
||||||
name="static",
|
# check if port is being used
|
||||||
)
|
if is_port_in_use(port, host):
|
||||||
|
port = get_free_port(port)
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
"bind": f"{host}:{port}",
|
"bind": f"{host}:{port}",
|
||||||
"workers": get_number_of_workers(workers),
|
"workers": get_number_of_workers(workers),
|
||||||
|
|
@ -152,7 +159,8 @@ def serve(
|
||||||
status_code = 0
|
status_code = 0
|
||||||
while status_code != 200:
|
while status_code != 200:
|
||||||
try:
|
try:
|
||||||
status_code = httpx.get(f"http://{host}:{port}").status_code
|
status_code = httpx.get(f"http://{host}:{port}/health").status_code
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
@ -161,11 +169,64 @@ def serve(
|
||||||
webbrowser.open(f"http://{host}:{port}")
|
webbrowser.open(f"http://{host}:{port}")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_static_files(app: FastAPI, static_files_dir: Path):
|
||||||
|
"""
|
||||||
|
Setup the static files directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app (FastAPI): FastAPI app.
|
||||||
|
path (str): Path to the static files directory.
|
||||||
|
"""
|
||||||
|
app.mount(
|
||||||
|
"/",
|
||||||
|
StaticFiles(directory=static_files_dir, html=True),
|
||||||
|
name="static",
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(404)
|
||||||
|
async def custom_404_handler(request, __):
|
||||||
|
path = static_files_dir / "index.html"
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise RuntimeError(f"File at path {path} does not exist.")
|
||||||
|
return FileResponse(path)
|
||||||
|
|
||||||
|
|
||||||
|
def is_port_in_use(port, host="localhost"):
|
||||||
|
"""
|
||||||
|
Check if a port is in use.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port (int): The port number to check.
|
||||||
|
host (str): The host to check the port on. Defaults to 'localhost'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the port is in use, False otherwise.
|
||||||
|
"""
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
return s.connect_ex((host, port)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_free_port(port):
|
||||||
|
"""
|
||||||
|
Given a used port, find a free port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port (int): The port number to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: A free port number.
|
||||||
|
"""
|
||||||
|
while is_port_in_use(port):
|
||||||
|
port += 1
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
def print_banner(host, port):
|
def print_banner(host, port):
|
||||||
# console = Console()
|
# console = Console()
|
||||||
|
|
||||||
word = "LangFlow"
|
word = "LangFlow"
|
||||||
colors = ["#690080", "#660099", "#4d00b3", "#3300cc", "#1a00e6", "#0000ff"]
|
colors = ["#3300cc"]
|
||||||
|
|
||||||
styled_word = ""
|
styled_word = ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
# Router for base api
|
# Router for base api
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from langflow.api.v1 import chat_router, endpoints_router, validate_router
|
from langflow.api.v1 import (
|
||||||
|
chat_router,
|
||||||
|
endpoints_router,
|
||||||
|
validate_router,
|
||||||
|
flows_router,
|
||||||
|
flow_styles_router,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["api"])
|
router = APIRouter(
|
||||||
|
prefix="/api/v1",
|
||||||
|
)
|
||||||
router.include_router(chat_router)
|
router.include_router(chat_router)
|
||||||
router.include_router(endpoints_router)
|
router.include_router(endpoints_router)
|
||||||
router.include_router(validate_router)
|
router.include_router(validate_router)
|
||||||
|
router.include_router(flows_router)
|
||||||
|
router.include_router(flow_styles_router)
|
||||||
|
|
|
||||||
24
src/backend/langflow/api/utils.py
Normal file
24
src/backend/langflow/api/utils.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
API_WORDS = ["api", "key", "token"]
|
||||||
|
|
||||||
|
|
||||||
|
def has_api_terms(word: str):
|
||||||
|
return "api" in word and (
|
||||||
|
"key" in word or ("token" in word and "tokens" not in word)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_api_keys(flow: dict):
|
||||||
|
"""Remove api keys from flow data."""
|
||||||
|
if flow.get("data") and flow["data"].get("nodes"):
|
||||||
|
for node in flow["data"]["nodes"]:
|
||||||
|
node_data = node.get("data").get("node")
|
||||||
|
template = node_data.get("template")
|
||||||
|
for value in template.values():
|
||||||
|
if (
|
||||||
|
isinstance(value, dict)
|
||||||
|
and has_api_terms(value["name"])
|
||||||
|
and value.get("password")
|
||||||
|
):
|
||||||
|
value["value"] = None
|
||||||
|
|
||||||
|
return flow
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
from langflow.api.v1.endpoints import router as endpoints_router
|
from langflow.api.v1.endpoints import router as endpoints_router
|
||||||
from langflow.api.v1.validate import router as validate_router
|
from langflow.api.v1.validate import router as validate_router
|
||||||
from langflow.api.v1.chat import router as chat_router
|
from langflow.api.v1.chat import router as chat_router
|
||||||
|
from langflow.api.v1.flows import router as flows_router
|
||||||
|
from langflow.api.v1.flow_styles import router as flow_styles_router
|
||||||
|
|
||||||
__all__ = ["chat_router", "endpoints_router", "validate_router"]
|
__all__ = [
|
||||||
|
"chat_router",
|
||||||
|
"endpoints_router",
|
||||||
|
"validate_router",
|
||||||
|
"flows_router",
|
||||||
|
"flow_styles_router",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,122 @@
|
||||||
|
import json
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
|
HTTPException,
|
||||||
WebSocket,
|
WebSocket,
|
||||||
WebSocketDisconnect,
|
|
||||||
WebSocketException,
|
WebSocketException,
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from langflow.api.v1.schemas import BuiltResponse, InitResponse
|
||||||
|
|
||||||
from langflow.chat.manager import ChatManager
|
from langflow.chat.manager import ChatManager
|
||||||
|
from langflow.graph.graph.base import Graph
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
|
from cachetools import LRUCache
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Chat"])
|
||||||
chat_manager = ChatManager()
|
chat_manager = ChatManager()
|
||||||
|
flow_data_store: LRUCache = LRUCache(maxsize=10)
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/chat/{client_id}")
|
@router.websocket("/chat/{client_id}")
|
||||||
async def websocket_endpoint(client_id: str, websocket: WebSocket):
|
async def chat(client_id: str, websocket: WebSocket):
|
||||||
"""Websocket endpoint for chat."""
|
"""Websocket endpoint for chat."""
|
||||||
try:
|
try:
|
||||||
|
if client_id in chat_manager.in_memory_cache:
|
||||||
await chat_manager.handle_websocket(client_id, websocket)
|
await chat_manager.handle_websocket(client_id, websocket)
|
||||||
|
else:
|
||||||
|
message = "Please, build the flow before sending messages"
|
||||||
|
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason=message)
|
||||||
except WebSocketException as exc:
|
except WebSocketException as exc:
|
||||||
logger.error(exc)
|
logger.error(exc)
|
||||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
||||||
except WebSocketDisconnect as exc:
|
|
||||||
|
|
||||||
|
@router.post("/build/init", response_model=InitResponse, status_code=201)
|
||||||
|
async def init_build(graph_data: dict):
|
||||||
|
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
flow_id = graph_data.get("id")
|
||||||
|
if flow_id is None:
|
||||||
|
raise ValueError("No ID provided")
|
||||||
|
flow_data_store[flow_id] = graph_data
|
||||||
|
|
||||||
|
return InitResponse(flowId=flow_id)
|
||||||
|
except Exception as exc:
|
||||||
logger.error(exc)
|
logger.error(exc)
|
||||||
await websocket.close(code=status.WS_1000_NORMAL_CLOSURE, reason=str(exc))
|
return HTTPException(status_code=500, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/build/{flow_id}/status", response_model=BuiltResponse)
|
||||||
|
async def build_status(flow_id: str):
|
||||||
|
"""Check the flow_id is in the flow_data_store."""
|
||||||
|
try:
|
||||||
|
built = flow_id in flow_data_store and not isinstance(
|
||||||
|
flow_data_store[flow_id], dict
|
||||||
|
)
|
||||||
|
|
||||||
|
return BuiltResponse(
|
||||||
|
built=built,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
return HTTPException(status_code=500, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
||||||
|
async def stream_build(flow_id: str):
|
||||||
|
"""Stream the build process based on stored flow data."""
|
||||||
|
|
||||||
|
async def event_stream(flow_id):
|
||||||
|
final_response = json.dumps({"end_of_stream": True})
|
||||||
|
try:
|
||||||
|
if flow_id not in flow_data_store:
|
||||||
|
error_message = "Invalid session ID"
|
||||||
|
yield f"data: {json.dumps({'error': error_message})}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
|
graph_data = flow_data_store[flow_id].get("data")
|
||||||
|
|
||||||
|
if not graph_data:
|
||||||
|
error_message = "No data provided"
|
||||||
|
yield f"data: {json.dumps({'error': error_message})}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Building langchain object")
|
||||||
|
graph = Graph.from_payload(graph_data)
|
||||||
|
for node in graph.generator_build():
|
||||||
|
try:
|
||||||
|
node.build()
|
||||||
|
params = node._built_object_repr()
|
||||||
|
valid = True
|
||||||
|
logger.debug(
|
||||||
|
f"Building node {params[:50]}{'...' if len(params) > 50 else ''}"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
params = str(exc)
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
response = json.dumps(
|
||||||
|
{
|
||||||
|
"valid": valid,
|
||||||
|
"params": params,
|
||||||
|
"id": node.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
yield f"data: {response}\n\n"
|
||||||
|
|
||||||
|
chat_manager.set_cache(flow_id, graph.build())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error while building the flow: %s", exc)
|
||||||
|
yield f"error: {json.dumps({'error': str(exc)})}\n\n"
|
||||||
|
finally:
|
||||||
|
yield f"data: {final_response}\n\n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
raise HTTPException(status_code=500, detail=str(exc))
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import logging
|
from langflow.database.models.flow import Flow
|
||||||
from importlib.metadata import version
|
from langflow.processing.process import process_graph_cached, process_tweaks
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from langflow.api.v1.schemas import (
|
from langflow.api.v1.schemas import (
|
||||||
ExportedFlow,
|
|
||||||
GraphData,
|
|
||||||
PredictRequest,
|
PredictRequest,
|
||||||
PredictResponse,
|
PredictResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
from langflow.interface.types import build_langchain_types_dict
|
from langflow.interface.types import build_langchain_types_dict
|
||||||
|
from langflow.database.base import get_session
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
# build router
|
# build router
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Base"])
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all")
|
@router.get("/all")
|
||||||
|
|
@ -22,16 +22,34 @@ def get_all():
|
||||||
return build_langchain_types_dict()
|
return build_langchain_types_dict()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/predict", response_model=PredictResponse)
|
@router.post("/predict/{flow_id}", response_model=PredictResponse)
|
||||||
async def get_load(predict_request: PredictRequest):
|
async def predict_flow(
|
||||||
try:
|
predict_request: PredictRequest,
|
||||||
from langflow.processing.process import process_graph_cached
|
flow_id: str,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Endpoint to process a message using the flow passed in the bearer token.
|
||||||
|
"""
|
||||||
|
|
||||||
exported_flow: ExportedFlow = predict_request.exported_flow
|
try:
|
||||||
graph_data: GraphData = exported_flow.data
|
flow = session.get(Flow, flow_id)
|
||||||
data = graph_data.dict()
|
if flow is None:
|
||||||
response = process_graph_cached(data, predict_request.message)
|
raise ValueError(f"Flow {flow_id} not found")
|
||||||
return PredictResponse(result=response.get("result", ""))
|
|
||||||
|
if flow.data is None:
|
||||||
|
raise ValueError(f"Flow {flow_id} has no data")
|
||||||
|
graph_data = flow.data
|
||||||
|
if predict_request.tweaks:
|
||||||
|
try:
|
||||||
|
graph_data = process_tweaks(graph_data, predict_request.tweaks)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Error processing tweaks: {exc}")
|
||||||
|
response = process_graph_cached(graph_data, predict_request.message)
|
||||||
|
return PredictResponse(
|
||||||
|
result=response.get("result", ""),
|
||||||
|
intermediate_steps=response.get("thought", ""),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log stack trace
|
# Log stack trace
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
|
||||||
83
src/backend/langflow/api/v1/flow_styles.py
Normal file
83
src/backend/langflow/api/v1/flow_styles.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from langflow.database.models.flow_style import (
|
||||||
|
FlowStyle,
|
||||||
|
FlowStyleCreate,
|
||||||
|
FlowStyleRead,
|
||||||
|
FlowStyleUpdate,
|
||||||
|
)
|
||||||
|
from langflow.database.base import get_session
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
# build router
|
||||||
|
router = APIRouter(prefix="/flow_styles", tags=["FlowStyles"])
|
||||||
|
|
||||||
|
# FlowStyleCreate:
|
||||||
|
# class FlowStyleBase(SQLModel):
|
||||||
|
# color: str = Field(index=True)
|
||||||
|
# emoji: str = Field(index=False)
|
||||||
|
# flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=FlowStyleRead)
|
||||||
|
def create_flow_style(
|
||||||
|
*, session: Session = Depends(get_session), flow_style: FlowStyleCreate
|
||||||
|
):
|
||||||
|
"""Create a new flow_style."""
|
||||||
|
db_flow_style = FlowStyle.from_orm(flow_style)
|
||||||
|
session.add(db_flow_style)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow_style)
|
||||||
|
return db_flow_style
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[FlowStyleRead])
|
||||||
|
def read_flow_styles(*, session: Session = Depends(get_session)):
|
||||||
|
"""Read all flows."""
|
||||||
|
try:
|
||||||
|
flows = session.exec(select(FlowStyle)).all()
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return flows
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{flow_styles_id}", response_model=FlowStyleRead)
|
||||||
|
def read_flow_style(*, session: Session = Depends(get_session), flow_styles_id: UUID):
|
||||||
|
"""Read a flow_style."""
|
||||||
|
if flow_style := session.get(FlowStyle, flow_styles_id):
|
||||||
|
return flow_style
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{flow_style_id}", response_model=FlowStyleRead)
|
||||||
|
def update_flow_style(
|
||||||
|
*,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
flow_style_id: UUID,
|
||||||
|
flow_style: FlowStyleUpdate,
|
||||||
|
):
|
||||||
|
"""Update a flow_style."""
|
||||||
|
db_flow_style = session.get(FlowStyle, flow_style_id)
|
||||||
|
if not db_flow_style:
|
||||||
|
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||||
|
flow_data = flow_style.dict(exclude_unset=True)
|
||||||
|
for key, value in flow_data.items():
|
||||||
|
if hasattr(db_flow_style, key) and value is not None:
|
||||||
|
setattr(db_flow_style, key, value)
|
||||||
|
session.add(db_flow_style)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow_style)
|
||||||
|
return db_flow_style
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{flow_id}")
|
||||||
|
def delete_flow_style(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||||
|
"""Delete a flow_style."""
|
||||||
|
flow_style = session.get(FlowStyle, flow_id)
|
||||||
|
if not flow_style:
|
||||||
|
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||||
|
session.delete(flow_style)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "FlowStyle deleted successfully"}
|
||||||
120
src/backend/langflow/api/v1/flows.py
Normal file
120
src/backend/langflow/api/v1/flows.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
from typing import List
|
||||||
|
from uuid import UUID
|
||||||
|
from langflow.settings import settings
|
||||||
|
from langflow.api.utils import remove_api_keys
|
||||||
|
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||||
|
from langflow.database.models.flow import (
|
||||||
|
Flow,
|
||||||
|
FlowCreate,
|
||||||
|
FlowRead,
|
||||||
|
FlowReadWithStyle,
|
||||||
|
FlowUpdate,
|
||||||
|
)
|
||||||
|
from langflow.database.base import get_session
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
|
from fastapi import File, UploadFile
|
||||||
|
import json
|
||||||
|
|
||||||
|
# build router
|
||||||
|
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=FlowRead, status_code=201)
|
||||||
|
def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate):
|
||||||
|
"""Create a new flow."""
|
||||||
|
db_flow = Flow.from_orm(flow)
|
||||||
|
session.add(db_flow)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[FlowReadWithStyle], status_code=200)
|
||||||
|
def read_flows(*, session: Session = Depends(get_session)):
|
||||||
|
"""Read all flows."""
|
||||||
|
try:
|
||||||
|
flows = session.exec(select(Flow)).all()
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return [jsonable_encoder(flow) for flow in flows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{flow_id}", response_model=FlowReadWithStyle, status_code=200)
|
||||||
|
def read_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||||
|
"""Read a flow."""
|
||||||
|
if flow := session.get(Flow, flow_id):
|
||||||
|
return flow
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||||
|
def update_flow(
|
||||||
|
*, session: Session = Depends(get_session), flow_id: UUID, flow: FlowUpdate
|
||||||
|
):
|
||||||
|
"""Update a flow."""
|
||||||
|
|
||||||
|
db_flow = session.get(Flow, flow_id)
|
||||||
|
if not db_flow:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
flow_data = flow.dict(exclude_unset=True)
|
||||||
|
if not settings.remove_api_keys:
|
||||||
|
flow_data = remove_api_keys(flow_data)
|
||||||
|
for key, value in flow_data.items():
|
||||||
|
setattr(db_flow, key, value)
|
||||||
|
session.add(db_flow)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{flow_id}", status_code=200)
|
||||||
|
def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||||
|
"""Delete a flow."""
|
||||||
|
flow = session.get(Flow, flow_id)
|
||||||
|
if not flow:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
session.delete(flow)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Flow deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# Define a new model to handle multiple flows
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/batch/", response_model=List[FlowRead], status_code=201)
|
||||||
|
def create_flows(*, session: Session = Depends(get_session), flow_list: FlowListCreate):
|
||||||
|
"""Create multiple new flows."""
|
||||||
|
db_flows = []
|
||||||
|
for flow in flow_list.flows:
|
||||||
|
db_flow = Flow.from_orm(flow)
|
||||||
|
session.add(db_flow)
|
||||||
|
db_flows.append(db_flow)
|
||||||
|
session.commit()
|
||||||
|
for db_flow in db_flows:
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flows
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload/", response_model=List[FlowRead], status_code=201)
|
||||||
|
async def upload_file(
|
||||||
|
*, session: Session = Depends(get_session), file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
"""Upload flows from a file."""
|
||||||
|
contents = await file.read()
|
||||||
|
data = json.loads(contents)
|
||||||
|
if "flows" in data:
|
||||||
|
flow_list = FlowListCreate(**data)
|
||||||
|
else:
|
||||||
|
flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data])
|
||||||
|
return create_flows(session=session, flow_list=flow_list)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/download/", response_model=FlowListRead, status_code=200)
|
||||||
|
async def download_file(*, session: Session = Depends(get_session)):
|
||||||
|
"""Download all flows as a file."""
|
||||||
|
flows = read_flows(session=session)
|
||||||
|
return FlowListRead(flows=flows)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
from langflow.database.models.flow import FlowCreate, FlowRead
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
|
|
||||||
class GraphData(BaseModel):
|
class GraphData(BaseModel):
|
||||||
|
|
@ -23,13 +23,30 @@ class PredictRequest(BaseModel):
|
||||||
"""Predict request schema."""
|
"""Predict request schema."""
|
||||||
|
|
||||||
message: str
|
message: str
|
||||||
exported_flow: ExportedFlow
|
tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"message": "Hello, how are you?",
|
||||||
|
"tweaks": {
|
||||||
|
"dndnode_986363f0-4677-4035-9f38-74b94af5dd78": {
|
||||||
|
"name": "A tool name",
|
||||||
|
"description": "A tool description",
|
||||||
|
},
|
||||||
|
"dndnode_986363f0-4677-4035-9f38-74b94af57378": {
|
||||||
|
"template": "A {template}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PredictResponse(BaseModel):
|
class PredictResponse(BaseModel):
|
||||||
"""Predict response schema."""
|
"""Predict response schema."""
|
||||||
|
|
||||||
result: str
|
result: str
|
||||||
|
intermediate_steps: str = ""
|
||||||
|
|
||||||
|
|
||||||
class ChatMessage(BaseModel):
|
class ChatMessage(BaseModel):
|
||||||
|
|
@ -68,3 +85,19 @@ class FileResponse(ChatMessage):
|
||||||
if v not in ["image", "csv"]:
|
if v not in ["image", "csv"]:
|
||||||
raise ValueError("data_type must be image or csv")
|
raise ValueError("data_type must be image or csv")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class FlowListCreate(BaseModel):
|
||||||
|
flows: List[FlowCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class FlowListRead(BaseModel):
|
||||||
|
flows: List[FlowRead]
|
||||||
|
|
||||||
|
|
||||||
|
class InitResponse(BaseModel):
|
||||||
|
flowId: str
|
||||||
|
|
||||||
|
|
||||||
|
class BuiltResponse(BaseModel):
|
||||||
|
built: bool
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from langflow.utils.logger import logger
|
||||||
from langflow.utils.validate import validate_code
|
from langflow.utils.validate import validate_code
|
||||||
|
|
||||||
# build router
|
# build router
|
||||||
router = APIRouter(prefix="/validate", tags=["validate"])
|
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/code", status_code=200, response_model=CodeValidationResponse)
|
@router.post("/code", status_code=200, response_model=CodeValidationResponse)
|
||||||
|
|
|
||||||
8
src/backend/langflow/cache/__init__.py
vendored
8
src/backend/langflow/cache/__init__.py
vendored
|
|
@ -1 +1,7 @@
|
||||||
from langflow.cache.manager import cache_manager # noqa
|
from langflow.cache.manager import cache_manager
|
||||||
|
from langflow.cache.flow import InMemoryCache
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"cache_manager",
|
||||||
|
"InMemoryCache",
|
||||||
|
]
|
||||||
|
|
|
||||||
207
src/backend/langflow/cache/base.py
vendored
207
src/backend/langflow/cache/base.py
vendored
|
|
@ -1,154 +1,87 @@
|
||||||
import base64
|
import abc
|
||||||
import contextlib
|
|
||||||
import functools
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
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):
|
class BaseCache(abc.ABC):
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
# Get the destination folder
|
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
|
||||||
|
|
||||||
# Create the destination folder if it doesn't exist
|
|
||||||
os.makedirs(cache_path, exist_ok=True)
|
|
||||||
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def memoize_dict(maxsize=128):
|
|
||||||
cache = OrderedDict()
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
hashed = compute_dict_hash(args[0])
|
|
||||||
key = (func.__name__, hashed, frozenset(kwargs.items()))
|
|
||||||
if key not in cache:
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
cache[key] = result
|
|
||||||
if len(cache) > maxsize:
|
|
||||||
cache.popitem(last=False)
|
|
||||||
else:
|
|
||||||
result = cache[key]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def clear_cache():
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
wrapper.clear_cache = clear_cache # type: ignore
|
|
||||||
wrapper.cache = cache # type: ignore
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
PREFIX = "langflow_cache"
|
|
||||||
|
|
||||||
|
|
||||||
@create_cache_folder
|
|
||||||
def clear_old_cache_files(max_cache_size: int = 3):
|
|
||||||
cache_dir = Path(tempfile.gettempdir()) / PREFIX
|
|
||||||
cache_files = list(cache_dir.glob("*.dill"))
|
|
||||||
|
|
||||||
if len(cache_files) > max_cache_size:
|
|
||||||
cache_files_sorted_by_mtime = sorted(
|
|
||||||
cache_files, key=lambda x: x.stat().st_mtime, reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
for cache_file in cache_files_sorted_by_mtime[max_cache_size:]:
|
|
||||||
with contextlib.suppress(OSError):
|
|
||||||
os.remove(cache_file)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_dict_hash(graph_data):
|
|
||||||
graph_data = filter_json(graph_data)
|
|
||||||
|
|
||||||
cleaned_graph_json = json.dumps(graph_data, sort_keys=True)
|
|
||||||
return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def filter_json(json_data):
|
|
||||||
filtered_data = json_data.copy()
|
|
||||||
|
|
||||||
# Remove 'viewport' and 'chatHistory' keys
|
|
||||||
if "viewport" in filtered_data:
|
|
||||||
del filtered_data["viewport"]
|
|
||||||
if "chatHistory" in filtered_data:
|
|
||||||
del filtered_data["chatHistory"]
|
|
||||||
|
|
||||||
# Filter nodes
|
|
||||||
if "nodes" in filtered_data:
|
|
||||||
for node in filtered_data["nodes"]:
|
|
||||||
if "position" in node:
|
|
||||||
del node["position"]
|
|
||||||
if "positionAbsolute" in node:
|
|
||||||
del node["positionAbsolute"]
|
|
||||||
if "selected" in node:
|
|
||||||
del node["selected"]
|
|
||||||
if "dragging" in node:
|
|
||||||
del node["dragging"]
|
|
||||||
|
|
||||||
return filtered_data
|
|
||||||
|
|
||||||
|
|
||||||
@create_cache_folder
|
|
||||||
def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str:
|
|
||||||
"""
|
"""
|
||||||
Save a binary file to the specified folder.
|
Abstract base class for a cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get(self, key):
|
||||||
|
"""
|
||||||
|
Retrieve an item from the cache.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: The content of the file as a bytes object.
|
key: The key of the item to retrieve.
|
||||||
file_name: The name of the file, including its extension.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The path to the saved file.
|
The value associated with the key, or None if the key is not found.
|
||||||
"""
|
"""
|
||||||
if not any(file_name.endswith(suffix) for suffix in accepted_types):
|
|
||||||
raise ValueError(f"File {file_name} is not accepted")
|
|
||||||
|
|
||||||
# Get the destination folder
|
@abc.abstractmethod
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
def set(self, key, value):
|
||||||
if not content:
|
"""
|
||||||
raise ValueError("Please, reload the file in the loader.")
|
Add an item to the cache.
|
||||||
data = content.split(",")[1]
|
|
||||||
decoded_bytes = base64.b64decode(data)
|
|
||||||
|
|
||||||
# Create the full file path
|
Args:
|
||||||
file_path = os.path.join(cache_path, file_name)
|
key: The key of the item.
|
||||||
|
value: The value to cache.
|
||||||
|
"""
|
||||||
|
|
||||||
# Save the binary content to the file
|
@abc.abstractmethod
|
||||||
with open(file_path, "wb") as file:
|
def delete(self, key):
|
||||||
file.write(decoded_bytes)
|
"""
|
||||||
|
Remove an item from the cache.
|
||||||
|
|
||||||
return file_path
|
Args:
|
||||||
|
key: The key of the item to remove.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Clear all items from the cache.
|
||||||
|
"""
|
||||||
|
|
||||||
@create_cache_folder
|
@abc.abstractmethod
|
||||||
def save_cache(hash_val: str, chat_data, clean_old_cache_files: bool):
|
def __contains__(self, key):
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
"""
|
||||||
with cache_path.open("wb") as cache_file:
|
Check if the key is in the cache.
|
||||||
dill.dump(chat_data, cache_file)
|
|
||||||
|
|
||||||
if clean_old_cache_files:
|
Args:
|
||||||
clear_old_cache_files()
|
key: The key of the item to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the key is in the cache, False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
@create_cache_folder
|
@abc.abstractmethod
|
||||||
def load_cache(hash_val):
|
def __getitem__(self, key):
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
"""
|
||||||
if cache_path.exists():
|
Retrieve an item from the cache using the square bracket notation.
|
||||||
with cache_path.open("rb") as cache_file:
|
|
||||||
return dill.load(cache_file)
|
Args:
|
||||||
return None
|
key: The key of the item to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The value associated with the key, or None if the key is not found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""
|
||||||
|
Add an item to the cache using the square bracket notation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item.
|
||||||
|
value: The value to cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"""
|
||||||
|
Remove an item from the cache using the square bracket notation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to remove.
|
||||||
|
"""
|
||||||
|
|
|
||||||
146
src/backend/langflow/cache/flow.py
vendored
Normal file
146
src/backend/langflow/cache/flow.py
vendored
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from langflow.cache.base import BaseCache
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryCache(BaseCache):
|
||||||
|
"""
|
||||||
|
A simple in-memory cache using an OrderedDict.
|
||||||
|
|
||||||
|
This cache supports setting a maximum size and expiration time for cached items.
|
||||||
|
When the cache is full, it uses a Least Recently Used (LRU) eviction policy.
|
||||||
|
Thread-safe using a threading Lock.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
max_size (int, optional): Maximum number of items to store in the cache.
|
||||||
|
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
cache = InMemoryCache(max_size=3, expiration_time=5)
|
||||||
|
|
||||||
|
# setting cache values
|
||||||
|
cache.set("a", 1)
|
||||||
|
cache.set("b", 2)
|
||||||
|
cache["c"] = 3
|
||||||
|
|
||||||
|
# getting cache values
|
||||||
|
a = cache.get("a")
|
||||||
|
b = cache["b"]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_size=None, expiration_time=60 * 60):
|
||||||
|
"""
|
||||||
|
Initialize a new InMemoryCache instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_size (int, optional): Maximum number of items to store in the cache.
|
||||||
|
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||||
|
"""
|
||||||
|
self._cache = OrderedDict()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self.max_size = max_size
|
||||||
|
self.expiration_time = expiration_time
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
"""
|
||||||
|
Retrieve an item from the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The value associated with the key, or None if the key is not found or the item has expired.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if key in self._cache:
|
||||||
|
item = self._cache.pop(key)
|
||||||
|
if (
|
||||||
|
self.expiration_time is None
|
||||||
|
or time.time() - item["time"] < self.expiration_time
|
||||||
|
):
|
||||||
|
# Move the key to the end to make it recently used
|
||||||
|
self._cache[key] = item
|
||||||
|
return item["value"]
|
||||||
|
else:
|
||||||
|
self.delete(key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
"""
|
||||||
|
Add an item to the cache.
|
||||||
|
|
||||||
|
If the cache is full, the least recently used item is evicted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item.
|
||||||
|
value: The value to cache.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if key in self._cache:
|
||||||
|
# Remove existing key before re-inserting to update order
|
||||||
|
self.delete(key)
|
||||||
|
elif self.max_size and len(self._cache) >= self.max_size:
|
||||||
|
# Remove least recently used item
|
||||||
|
self._cache.popitem(last=False)
|
||||||
|
self._cache[key] = {"value": value, "time": time.time()}
|
||||||
|
|
||||||
|
def get_or_set(self, key, value):
|
||||||
|
"""
|
||||||
|
Retrieve an item from the cache. If the item does not exist, set it with the provided value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item.
|
||||||
|
value: The value to cache if the item doesn't exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The cached value associated with the key.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if key in self._cache:
|
||||||
|
return self.get(key)
|
||||||
|
self.set(key, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
"""
|
||||||
|
Remove an item from the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to remove.
|
||||||
|
"""
|
||||||
|
# with self._lock:
|
||||||
|
self._cache.pop(key, None)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Clear all items from the cache.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._cache.clear()
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
"""Check if the key is in the cache."""
|
||||||
|
return key in self._cache
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""Retrieve an item from the cache using the square bracket notation."""
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""Add an item to the cache using the square bracket notation."""
|
||||||
|
self.set(key, value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"""Remove an item from the cache using the square bracket notation."""
|
||||||
|
self.delete(key)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Return the number of items in the cache."""
|
||||||
|
return len(self._cache)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Return a string representation of the InMemoryCache instance."""
|
||||||
|
return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})"
|
||||||
6
src/backend/langflow/cache/manager.py
vendored
6
src/backend/langflow/cache/manager.py
vendored
|
|
@ -54,7 +54,7 @@ class CacheManager(Subject):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.CACHE = {}
|
self._cache = {}
|
||||||
self.current_client_id = None
|
self.current_client_id = None
|
||||||
self.current_cache = {}
|
self.current_cache = {}
|
||||||
|
|
||||||
|
|
@ -68,12 +68,12 @@ class CacheManager(Subject):
|
||||||
"""
|
"""
|
||||||
previous_client_id = self.current_client_id
|
previous_client_id = self.current_client_id
|
||||||
self.current_client_id = client_id
|
self.current_client_id = client_id
|
||||||
self.current_cache = self.CACHE.setdefault(client_id, {})
|
self.current_cache = self._cache.setdefault(client_id, {})
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self.current_client_id = previous_client_id
|
self.current_client_id = previous_client_id
|
||||||
self.current_cache = self.CACHE.get(self.current_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):
|
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
134
src/backend/langflow/cache/utils.py
vendored
Normal file
134
src/backend/langflow/cache/utils.py
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import base64
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
CACHE: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def create_cache_folder(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Get the destination folder
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
|
||||||
|
# Create the destination folder if it doesn't exist
|
||||||
|
os.makedirs(cache_path, exist_ok=True)
|
||||||
|
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def memoize_dict(maxsize=128):
|
||||||
|
cache = OrderedDict()
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
hashed = compute_dict_hash(args[0])
|
||||||
|
key = (func.__name__, hashed, frozenset(kwargs.items()))
|
||||||
|
if key not in cache:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
cache[key] = result
|
||||||
|
if len(cache) > maxsize:
|
||||||
|
cache.popitem(last=False)
|
||||||
|
else:
|
||||||
|
result = cache[key]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clear_cache():
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
wrapper.clear_cache = clear_cache # type: ignore
|
||||||
|
wrapper.cache = cache # type: ignore
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
PREFIX = "langflow_cache"
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def clear_old_cache_files(max_cache_size: int = 3):
|
||||||
|
cache_dir = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
cache_files = list(cache_dir.glob("*.dill"))
|
||||||
|
|
||||||
|
if len(cache_files) > max_cache_size:
|
||||||
|
cache_files_sorted_by_mtime = sorted(
|
||||||
|
cache_files, key=lambda x: x.stat().st_mtime, reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for cache_file in cache_files_sorted_by_mtime[max_cache_size:]:
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
os.remove(cache_file)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_dict_hash(graph_data):
|
||||||
|
graph_data = filter_json(graph_data)
|
||||||
|
|
||||||
|
cleaned_graph_json = json.dumps(graph_data, sort_keys=True)
|
||||||
|
return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def filter_json(json_data):
|
||||||
|
filtered_data = json_data.copy()
|
||||||
|
|
||||||
|
# Remove 'viewport' and 'chatHistory' keys
|
||||||
|
if "viewport" in filtered_data:
|
||||||
|
del filtered_data["viewport"]
|
||||||
|
if "chatHistory" in filtered_data:
|
||||||
|
del filtered_data["chatHistory"]
|
||||||
|
|
||||||
|
# Filter nodes
|
||||||
|
if "nodes" in filtered_data:
|
||||||
|
for node in filtered_data["nodes"]:
|
||||||
|
if "position" in node:
|
||||||
|
del node["position"]
|
||||||
|
if "positionAbsolute" in node:
|
||||||
|
del node["positionAbsolute"]
|
||||||
|
if "selected" in node:
|
||||||
|
del node["selected"]
|
||||||
|
if "dragging" in node:
|
||||||
|
del node["dragging"]
|
||||||
|
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str:
|
||||||
|
"""
|
||||||
|
Save a binary file to the specified folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The content of the file as a bytes object.
|
||||||
|
file_name: The name of the file, including its extension.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The path to the saved file.
|
||||||
|
"""
|
||||||
|
if not any(file_name.endswith(suffix) for suffix in accepted_types):
|
||||||
|
raise ValueError(f"File {file_name} is not accepted")
|
||||||
|
|
||||||
|
# Get the destination folder
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
if not content:
|
||||||
|
raise ValueError("Please, reload the file in the loader.")
|
||||||
|
data = content.split(",")[1]
|
||||||
|
decoded_bytes = base64.b64decode(data)
|
||||||
|
|
||||||
|
# Create the full file path
|
||||||
|
file_path = os.path.join(cache_path, file_name)
|
||||||
|
|
||||||
|
# Save the binary content to the file
|
||||||
|
with open(file_path, "wb") as file:
|
||||||
|
file.write(decoded_bytes)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
@ -10,7 +10,9 @@ from langflow.utils.logger import logger
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from langflow.cache.flow import InMemoryCache
|
||||||
|
|
||||||
|
|
||||||
class ChatHistory(Subject):
|
class ChatHistory(Subject):
|
||||||
|
|
@ -46,6 +48,7 @@ class ChatManager:
|
||||||
self.chat_history = ChatHistory()
|
self.chat_history = ChatHistory()
|
||||||
self.cache_manager = cache_manager
|
self.cache_manager = cache_manager
|
||||||
self.cache_manager.attach(self.update)
|
self.cache_manager.attach(self.update)
|
||||||
|
self.in_memory_cache = InMemoryCache()
|
||||||
|
|
||||||
def on_chat_history_update(self):
|
def on_chat_history_update(self):
|
||||||
"""Send the last chat message to the client."""
|
"""Send the last chat message to the client."""
|
||||||
|
|
@ -99,24 +102,30 @@ class ChatManager:
|
||||||
websocket = self.active_connections[client_id]
|
websocket = self.active_connections[client_id]
|
||||||
await websocket.send_json(message.dict())
|
await websocket.send_json(message.dict())
|
||||||
|
|
||||||
async def process_message(self, client_id: str, payload: Dict):
|
async def close_connection(self, client_id: str, code: int, reason: str):
|
||||||
|
if websocket := self.active_connections[client_id]:
|
||||||
|
await websocket.close(code=code, reason=reason)
|
||||||
|
self.disconnect(client_id)
|
||||||
|
|
||||||
|
async def process_message(
|
||||||
|
self, client_id: str, payload: Dict, langchain_object: Any
|
||||||
|
):
|
||||||
# Process the graph data and chat message
|
# Process the graph data and chat message
|
||||||
chat_message = payload.pop("message", "")
|
chat_message = payload.pop("message", "")
|
||||||
chat_message = ChatMessage(message=chat_message)
|
chat_message = ChatMessage(message=chat_message)
|
||||||
self.chat_history.add_message(client_id, chat_message)
|
self.chat_history.add_message(client_id, chat_message)
|
||||||
|
|
||||||
graph_data = payload
|
# graph_data = payload
|
||||||
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
|
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
|
||||||
await self.send_json(client_id, start_resp)
|
await self.send_json(client_id, start_resp)
|
||||||
|
|
||||||
is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1
|
# is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1
|
||||||
# Generate result and thought
|
# Generate result and thought
|
||||||
try:
|
try:
|
||||||
logger.debug("Generating result and thought")
|
logger.debug("Generating result and thought")
|
||||||
|
|
||||||
result, intermediate_steps = await process_graph(
|
result, intermediate_steps = await process_graph(
|
||||||
graph_data=graph_data,
|
langchain_object=langchain_object,
|
||||||
is_first_message=is_first_message,
|
|
||||||
chat_message=chat_message,
|
chat_message=chat_message,
|
||||||
websocket=self.active_connections[client_id],
|
websocket=self.active_connections[client_id],
|
||||||
)
|
)
|
||||||
|
|
@ -149,6 +158,14 @@ class ChatManager:
|
||||||
await self.send_json(client_id, response)
|
await self.send_json(client_id, response)
|
||||||
self.chat_history.add_message(client_id, response)
|
self.chat_history.add_message(client_id, response)
|
||||||
|
|
||||||
|
def set_cache(self, client_id: str, langchain_object: Any) -> bool:
|
||||||
|
"""
|
||||||
|
Set the cache for a client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.in_memory_cache.set(client_id, langchain_object)
|
||||||
|
return client_id in self.in_memory_cache
|
||||||
|
|
||||||
async def handle_websocket(self, client_id: str, websocket: WebSocket):
|
async def handle_websocket(self, client_id: str, websocket: WebSocket):
|
||||||
await self.connect(client_id, websocket)
|
await self.connect(client_id, websocket)
|
||||||
|
|
||||||
|
|
@ -169,22 +186,24 @@ class ChatManager:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with self.cache_manager.set_client_id(client_id):
|
with self.cache_manager.set_client_id(client_id):
|
||||||
await self.process_message(client_id, payload)
|
langchain_object = self.in_memory_cache.get(client_id)
|
||||||
|
await self.process_message(client_id, payload, langchain_object)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle any exceptions that might occur
|
# Handle any exceptions that might occur
|
||||||
logger.exception(e)
|
logger.error(e)
|
||||||
# send a message to the client
|
await self.close_connection(
|
||||||
await self.active_connections[client_id].close(
|
client_id=client_id,
|
||||||
code=status.WS_1011_INTERNAL_ERROR, reason=str(e)[:120]
|
code=status.WS_1011_INTERNAL_ERROR,
|
||||||
|
reason=str(e)[:120],
|
||||||
)
|
)
|
||||||
self.disconnect(client_id)
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
connection = self.active_connections.get(client_id)
|
await self.close_connection(
|
||||||
if connection:
|
client_id=client_id,
|
||||||
await connection.close(code=1000, reason="Client disconnected")
|
code=status.WS_1000_NORMAL_CLOSURE,
|
||||||
self.disconnect(client_id)
|
reason="Client disconnected",
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.error(e)
|
||||||
self.disconnect(client_id)
|
self.disconnect(client_id)
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,15 @@
|
||||||
from fastapi import WebSocket
|
from fastapi import WebSocket
|
||||||
from langflow.api.v1.schemas import ChatMessage
|
from langflow.api.v1.schemas import ChatMessage
|
||||||
from langflow.processing.process import (
|
|
||||||
load_or_build_langchain_object,
|
|
||||||
)
|
|
||||||
from langflow.processing.base import get_result_and_steps
|
from langflow.processing.base import get_result_and_steps
|
||||||
from langflow.interface.utils import try_setting_streaming_options
|
from langflow.interface.utils import try_setting_streaming_options
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
|
|
||||||
async def process_graph(
|
async def process_graph(
|
||||||
graph_data: Dict,
|
langchain_object,
|
||||||
is_first_message: bool,
|
|
||||||
chat_message: ChatMessage,
|
chat_message: ChatMessage,
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
):
|
):
|
||||||
langchain_object = load_or_build_langchain_object(graph_data, is_first_message)
|
|
||||||
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
||||||
logger.debug("Loaded langchain object")
|
logger.debug("Loaded langchain object")
|
||||||
|
|
||||||
|
|
|
||||||
0
src/backend/langflow/database/__init__.py
Normal file
0
src/backend/langflow/database/__init__.py
Normal file
18
src/backend/langflow/database/base.py
Normal file
18
src/backend/langflow/database/base.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from langflow.settings import settings
|
||||||
|
from sqlmodel import SQLModel, Session, create_engine
|
||||||
|
|
||||||
|
|
||||||
|
if settings.database_url.startswith("sqlite"):
|
||||||
|
connect_args = {"check_same_thread": False}
|
||||||
|
else:
|
||||||
|
connect_args = {}
|
||||||
|
engine = create_engine(settings.database_url, connect_args=connect_args)
|
||||||
|
|
||||||
|
|
||||||
|
def create_db_and_tables():
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
0
src/backend/langflow/database/models/__init__.py
Normal file
0
src/backend/langflow/database/models/__init__.py
Normal file
14
src/backend/langflow/database/models/base.py
Normal file
14
src/backend/langflow/database/models/base.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
|
||||||
|
def orjson_dumps(v, *, default):
|
||||||
|
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
|
||||||
|
return orjson.dumps(v, default=default).decode()
|
||||||
|
|
||||||
|
|
||||||
|
class SQLModelSerializable(SQLModel):
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
json_loads = orjson.loads
|
||||||
|
json_dumps = orjson_dumps
|
||||||
60
src/backend/langflow/database/models/flow.py
Normal file
60
src/backend/langflow/database/models/flow.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Path: src/backend/langflow/database/models/flow.py
|
||||||
|
|
||||||
|
from langflow.database.models.base import SQLModelSerializable
|
||||||
|
from pydantic import validator
|
||||||
|
from sqlmodel import Field, Relationship, JSON, Column
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
# if TYPE_CHECKING:
|
||||||
|
from langflow.database.models.flow_style import FlowStyle, FlowStyleRead
|
||||||
|
|
||||||
|
|
||||||
|
class FlowBase(SQLModelSerializable):
|
||||||
|
name: str = Field(index=True)
|
||||||
|
description: Optional[str] = Field(index=True)
|
||||||
|
data: Optional[Dict] = Field(default=None)
|
||||||
|
|
||||||
|
@validator("data")
|
||||||
|
def validate_json(v):
|
||||||
|
# dict_keys(['description', 'name', 'id', 'data'])
|
||||||
|
if not v:
|
||||||
|
return v
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
raise ValueError("Flow must be a valid JSON")
|
||||||
|
|
||||||
|
# data must contain nodes and edges
|
||||||
|
if "nodes" not in v.keys():
|
||||||
|
raise ValueError("Flow must have nodes")
|
||||||
|
if "edges" not in v.keys():
|
||||||
|
raise ValueError("Flow must have edges")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class Flow(FlowBase, table=True):
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||||
|
data: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
|
||||||
|
style: Optional["FlowStyle"] = Relationship(
|
||||||
|
back_populates="flow",
|
||||||
|
# use "uselist=False" to make it a one-to-one relationship
|
||||||
|
sa_relationship_kwargs={"uselist": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlowCreate(FlowBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FlowRead(FlowBase):
|
||||||
|
id: UUID
|
||||||
|
|
||||||
|
|
||||||
|
class FlowReadWithStyle(FlowRead):
|
||||||
|
style: Optional["FlowStyleRead"] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FlowUpdate(SQLModelSerializable):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
data: Optional[Dict] = None
|
||||||
33
src/backend/langflow/database/models/flow_style.py
Normal file
33
src/backend/langflow/database/models/flow_style.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Path: src/backend/langflow/database/models/flowstyle.py
|
||||||
|
|
||||||
|
from langflow.database.models.base import SQLModelSerializable
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langflow.database.models.flow import Flow
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleBase(SQLModelSerializable):
|
||||||
|
color: str
|
||||||
|
emoji: str
|
||||||
|
flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyle(FlowStyleBase, table=True):
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||||
|
flow: "Flow" = Relationship(back_populates="style")
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleUpdate(SQLModelSerializable):
|
||||||
|
color: Optional[str] = None
|
||||||
|
emoji: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleCreate(FlowStyleBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleRead(FlowStyleBase):
|
||||||
|
id: UUID
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, List, Type, Union
|
from typing import Dict, Generator, List, Type, Union
|
||||||
|
|
||||||
from langflow.graph.edge.base import Edge
|
from langflow.graph.edge.base import Edge
|
||||||
from langflow.graph.graph.constants import VERTEX_TYPE_MAP
|
from langflow.graph.graph.constants import VERTEX_TYPE_MAP
|
||||||
|
|
@ -31,7 +31,7 @@ class Graph:
|
||||||
Creates a graph from a payload.
|
Creates a graph from a payload.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
payload (Dict): The payload to create the graph from.
|
payload (Dict): The payload to create the graph from.˜`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Graph: The created graph.
|
Graph: The created graph.
|
||||||
|
|
@ -107,6 +107,47 @@ class Graph:
|
||||||
raise ValueError("No root node found")
|
raise ValueError("No root node found")
|
||||||
return root_node.build()
|
return root_node.build()
|
||||||
|
|
||||||
|
def topological_sort(self) -> List[Vertex]:
|
||||||
|
"""
|
||||||
|
Performs a topological sort of the vertices in the graph.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Vertex]: A list of vertices in topological order.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the graph contains a cycle.
|
||||||
|
"""
|
||||||
|
# States: 0 = unvisited, 1 = visiting, 2 = visited
|
||||||
|
state = {node: 0 for node in self.nodes}
|
||||||
|
sorted_vertices = []
|
||||||
|
|
||||||
|
def dfs(node):
|
||||||
|
if state[node] == 1:
|
||||||
|
# We have a cycle
|
||||||
|
raise ValueError(
|
||||||
|
"Graph contains a cycle, cannot perform topological sort"
|
||||||
|
)
|
||||||
|
if state[node] == 0:
|
||||||
|
state[node] = 1
|
||||||
|
for edge in node.edges:
|
||||||
|
if edge.source == node:
|
||||||
|
dfs(edge.target)
|
||||||
|
state[node] = 2
|
||||||
|
sorted_vertices.append(node)
|
||||||
|
|
||||||
|
# Visit each node
|
||||||
|
for node in self.nodes:
|
||||||
|
if state[node] == 0:
|
||||||
|
dfs(node)
|
||||||
|
|
||||||
|
return list(reversed(sorted_vertices))
|
||||||
|
|
||||||
|
def generator_build(self) -> Generator:
|
||||||
|
"""Builds each vertex in the graph and yields it."""
|
||||||
|
sorted_vertices = self.topological_sort()
|
||||||
|
logger.info("Sorted vertices: %s", sorted_vertices)
|
||||||
|
yield from sorted_vertices
|
||||||
|
|
||||||
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:
|
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:
|
||||||
"""Returns the neighbors of a node."""
|
"""Returns the neighbors of a node."""
|
||||||
neighbors: Dict[Vertex, int] = {}
|
neighbors: Dict[Vertex, int] = {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from langflow.cache import base as cache_utils
|
from langflow.cache import utils as cache_utils
|
||||||
from langflow.graph.vertex.constants import DIRECT_TYPES
|
from langflow.graph.vertex.constants import DIRECT_TYPES
|
||||||
from langflow.interface import loading
|
from langflow.interface import loading
|
||||||
from langflow.interface.listing import ALL_TYPES_DICT
|
from langflow.interface.listing import ALL_TYPES_DICT
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ class CSVAgent(CustomAgentExecutor):
|
||||||
|
|
||||||
|
|
||||||
class VectorStoreAgent(CustomAgentExecutor):
|
class VectorStoreAgent(CustomAgentExecutor):
|
||||||
"""Vector Store agent"""
|
"""Vector store agent"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def function_name():
|
def function_name():
|
||||||
|
|
@ -175,7 +175,7 @@ class SQLAgent(CustomAgentExecutor):
|
||||||
def from_toolkit_and_llm(
|
def from_toolkit_and_llm(
|
||||||
cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any
|
cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any
|
||||||
):
|
):
|
||||||
"""Construct a sql agent from an LLM and tools."""
|
"""Construct an SQL agent from an LLM and tools."""
|
||||||
db = SQLDatabase.from_uri(database_uri)
|
db = SQLDatabase.from_uri(database_uri)
|
||||||
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
|
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
|
||||||
|
|
||||||
|
|
@ -252,7 +252,11 @@ class VectorStoreRouterAgent(CustomAgentExecutor):
|
||||||
):
|
):
|
||||||
"""Construct a vector store router agent from an LLM and tools."""
|
"""Construct a vector store router agent from an LLM and tools."""
|
||||||
|
|
||||||
tools = vectorstoreroutertoolkit.get_tools()
|
tools = (
|
||||||
|
vectorstoreroutertoolkit
|
||||||
|
if isinstance(vectorstoreroutertoolkit, list)
|
||||||
|
else vectorstoreroutertoolkit.get_tools()
|
||||||
|
)
|
||||||
prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX)
|
prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX)
|
||||||
llm_chain = LLMChain(
|
llm_chain = LLMChain(
|
||||||
llm=llm,
|
llm=llm,
|
||||||
|
|
@ -302,7 +306,7 @@ class InitializeAgent(CustomAgentExecutor):
|
||||||
CUSTOM_AGENTS = {
|
CUSTOM_AGENTS = {
|
||||||
"JsonAgent": JsonAgent,
|
"JsonAgent": JsonAgent,
|
||||||
"CSVAgent": CSVAgent,
|
"CSVAgent": CSVAgent,
|
||||||
"initialize_agent": InitializeAgent,
|
"AgentInitializer": InitializeAgent,
|
||||||
"VectorStoreAgent": VectorStoreAgent,
|
"VectorStoreAgent": VectorStoreAgent,
|
||||||
"VectorStoreRouterAgent": VectorStoreRouterAgent,
|
"VectorStoreRouterAgent": VectorStoreRouterAgent,
|
||||||
"SQLAgent": SQLAgent,
|
"SQLAgent": SQLAgent,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ class ChainCreator(LangChainTypeCreator):
|
||||||
return ChainFrontendNode
|
return ChainFrontendNode
|
||||||
|
|
||||||
#! We need to find a better solution for this
|
#! We need to find a better solution for this
|
||||||
from_method_nodes = {"ConversationalRetrievalChain": "from_llm"}
|
from_method_nodes = {
|
||||||
|
"ConversationalRetrievalChain": "from_llm",
|
||||||
|
"LLMCheckerChain": "from_llm",
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type_to_loader_dict(self) -> Dict:
|
def type_to_loader_dict(self) -> Dict:
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,8 @@
|
||||||
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict
|
from langflow.cache.utils import memoize_dict
|
||||||
from langflow.graph import Graph
|
from langflow.graph import Graph
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
def load_langchain_object(data_graph, is_first_message=False):
|
|
||||||
"""
|
|
||||||
Load langchain object from cache if it exists, otherwise build it.
|
|
||||||
"""
|
|
||||||
computed_hash = compute_dict_hash(data_graph)
|
|
||||||
if is_first_message:
|
|
||||||
langchain_object = build_langchain_object(data_graph)
|
|
||||||
else:
|
|
||||||
logger.debug("Loading langchain object from cache")
|
|
||||||
langchain_object = load_cache(computed_hash)
|
|
||||||
|
|
||||||
return computed_hash, langchain_object
|
|
||||||
|
|
||||||
|
|
||||||
@memoize_dict(maxsize=10)
|
@memoize_dict(maxsize=10)
|
||||||
def build_langchain_object_with_caching(data_graph):
|
def build_langchain_object_with_caching(data_graph):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from langflow.api import router
|
from langflow.api import router
|
||||||
|
from langflow.database.base import create_db_and_tables
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
"""Create the FastAPI app and include the router."""
|
"""Create the FastAPI app and include the router."""
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
origins = [
|
origins = [
|
||||||
|
|
@ -25,6 +27,7 @@ def create_app():
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
app.on_event("startup")(create_db_and_tables)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,3 +170,28 @@ def load_flow_from_json(path: str, build=True):
|
||||||
fix_memory_inputs(langchain_object)
|
fix_memory_inputs(langchain_object)
|
||||||
return langchain_object
|
return langchain_object
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
def process_tweaks(graph_data: Dict, tweaks: Dict):
|
||||||
|
"""This function is used to tweak the graph data using the node id and the tweaks dict"""
|
||||||
|
# the tweaks dict is a dict of dicts
|
||||||
|
# the key is the node id and the value is a dict of the tweaks
|
||||||
|
# the dict of tweaks contains the name of a certain parameter and the value to be tweaked
|
||||||
|
|
||||||
|
# We need to process the graph data to add the tweaks
|
||||||
|
if "data" not in graph_data and "nodes" in graph_data:
|
||||||
|
nodes = graph_data["nodes"]
|
||||||
|
else:
|
||||||
|
nodes = graph_data["data"]["nodes"]
|
||||||
|
for node in nodes:
|
||||||
|
node_id = node["id"]
|
||||||
|
if node_id in tweaks:
|
||||||
|
node_tweaks = tweaks[node_id]
|
||||||
|
template_data = node["data"]["node"]["template"]
|
||||||
|
for tweak_name, tweake_value in node_tweaks.items():
|
||||||
|
if tweak_name in template_data:
|
||||||
|
template_data[tweak_name]["value"] = tweake_value
|
||||||
|
print(
|
||||||
|
f"Something changed in node {node_id} with tweak {tweak_name} and value {tweake_value}"
|
||||||
|
)
|
||||||
|
return graph_data
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,13 @@ class Settings(BaseSettings):
|
||||||
textsplitters: List[str] = []
|
textsplitters: List[str] = []
|
||||||
utilities: List[str] = []
|
utilities: List[str] = []
|
||||||
dev: bool = False
|
dev: bool = False
|
||||||
|
database_url: str = "sqlite:///./langflow.db"
|
||||||
|
remove_api_keys: bool = False
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
validate_assignment = True
|
validate_assignment = True
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
env_prefix = "LANGFLOW_"
|
||||||
|
|
||||||
@root_validator(allow_reuse=True)
|
@root_validator(allow_reuse=True)
|
||||||
def validate_lists(cls, values):
|
def validate_lists(cls, values):
|
||||||
|
|
@ -46,6 +49,11 @@ class Settings(BaseSettings):
|
||||||
self.utilities = new_settings.utilities or []
|
self.utilities = new_settings.utilities or []
|
||||||
self.dev = dev
|
self.dev = dev
|
||||||
|
|
||||||
|
def update_settings(self, **kwargs):
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if hasattr(self, key):
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
def save_settings_to_yaml(settings: Settings, file_path: str):
|
def save_settings_to_yaml(settings: Settings, file_path: str):
|
||||||
with open(file_path, "w") as f:
|
with open(file_path, "w") as f:
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class SQLAgentNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = """Construct a sql agent from an LLM and tools."""
|
description: str = """Construct an SQL agent from an LLM and tools."""
|
||||||
base_classes: list[str] = ["AgentExecutor"]
|
base_classes: list[str] = ["AgentExecutor"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
@ -155,6 +155,7 @@ class CSVAgentNode(FrontendNode):
|
||||||
|
|
||||||
class InitializeAgentNode(FrontendNode):
|
class InitializeAgentNode(FrontendNode):
|
||||||
name: str = "AgentInitializer"
|
name: str = "AgentInitializer"
|
||||||
|
display_name: str = "AgentInitializer"
|
||||||
template: Template = Template(
|
template: Template = Template(
|
||||||
type_name="initialize_agent",
|
type_name="initialize_agent",
|
||||||
fields=[
|
fields=[
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,14 @@ class ChainFrontendNode(FrontendNode):
|
||||||
field.show = True
|
field.show = True
|
||||||
field.advanced = True
|
field.advanced = True
|
||||||
|
|
||||||
|
# We should think of a way to deal with this later
|
||||||
|
# if field.field_type == "PromptTemplate":
|
||||||
|
# field.field_type = "str"
|
||||||
|
# field.multiline = True
|
||||||
|
# field.show = True
|
||||||
|
# field.advanced = False
|
||||||
|
# field.value = field.value.template
|
||||||
|
|
||||||
# Separated for possible future changes
|
# Separated for possible future changes
|
||||||
if field.name == "prompt" and field.value is None:
|
if field.name == "prompt" and field.value is None:
|
||||||
field.required = True
|
field.required = True
|
||||||
|
|
@ -126,7 +134,7 @@ class TimeTravelGuideChainNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = "Time travel guide chain to be used in the flow."
|
description: str = "Time travel guide chain."
|
||||||
base_classes: list[str] = [
|
base_classes: list[str] = [
|
||||||
"LLMChain",
|
"LLMChain",
|
||||||
"BaseCustomChain",
|
"BaseCustomChain",
|
||||||
|
|
@ -197,7 +205,7 @@ class CombineDocsChainNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = """Construct a zero shot agent from an LLM and tools."""
|
description: str = """Load question answering chain."""
|
||||||
base_classes: list[str] = ["BaseCombineDocumentsChain", "function"]
|
base_classes: list[str] = ["BaseCombineDocumentsChain", "function"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ from langflow.template.field.base import TemplateField
|
||||||
from langflow.template.frontend_node.base import FrontendNode
|
from langflow.template.frontend_node.base import FrontendNode
|
||||||
|
|
||||||
|
|
||||||
class DocumentLoaderFrontNode(FrontendNode):
|
|
||||||
@staticmethod
|
|
||||||
def build_template(
|
def build_template(
|
||||||
suffixes: list, fileTypes: list, name: str = "file_path"
|
suffixes: list, fileTypes: list, name: str = "file_path"
|
||||||
) -> TemplateField:
|
) -> TemplateField:
|
||||||
|
|
@ -18,6 +16,8 @@ class DocumentLoaderFrontNode(FrontendNode):
|
||||||
fileTypes=fileTypes,
|
fileTypes=fileTypes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentLoaderFrontNode(FrontendNode):
|
||||||
file_path_templates = {
|
file_path_templates = {
|
||||||
"AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
|
"AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
|
||||||
"CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]),
|
"CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ class LLMFrontendNode(FrontendNode):
|
||||||
field.name.title().replace("Openai", "OpenAI").replace("_", " ")
|
field.name.title().replace("Openai", "OpenAI").replace("_", " ")
|
||||||
).replace("Api", "API")
|
).replace("Api", "API")
|
||||||
|
|
||||||
|
if "key" not in field.name.lower() and "token" not in field.name.lower():
|
||||||
|
field.password = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_azure_field(field: TemplateField):
|
def format_azure_field(field: TemplateField):
|
||||||
if field.name == "model_name":
|
if field.name == "model_name":
|
||||||
|
|
@ -22,7 +25,6 @@ class LLMFrontendNode(FrontendNode):
|
||||||
field.value = "azure"
|
field.value = "azure"
|
||||||
elif field.name == "openai_api_version":
|
elif field.name == "openai_api_version":
|
||||||
field.password = False
|
field.password = False
|
||||||
field.value = "2023-03-15-preview"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_llama_field(field: TemplateField):
|
def format_llama_field(field: TemplateField):
|
||||||
|
|
@ -44,7 +46,10 @@ class LLMFrontendNode(FrontendNode):
|
||||||
if field.name in SHOW_FIELDS:
|
if field.name in SHOW_FIELDS:
|
||||||
field.show = True
|
field.show = True
|
||||||
|
|
||||||
if "api" in field.name and ("key" in field.name or "token" in field.name):
|
if "api" in field.name and (
|
||||||
|
"key" in field.name
|
||||||
|
or ("token" in field.name and "tokens" not in field.name)
|
||||||
|
):
|
||||||
field.password = True
|
field.password = True
|
||||||
field.show = True
|
field.show = True
|
||||||
# Required should be False to support
|
# Required should be False to support
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class ToolNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = "Tool to be used in the flow."
|
description: str = "Converts a chain, agent or function into a tool."
|
||||||
base_classes: list[str] = ["Tool"]
|
base_classes: list[str] = ["Tool"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,16 @@ OPENAI_MODELS = [
|
||||||
"text-babbage-001",
|
"text-babbage-001",
|
||||||
"text-ada-001",
|
"text-ada-001",
|
||||||
]
|
]
|
||||||
CHAT_OPENAI_MODELS = ["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"]
|
CHAT_OPENAI_MODELS = [
|
||||||
|
"gpt-3.5-turbo-0613",
|
||||||
|
"gpt-3.5-turbo",
|
||||||
|
"gpt-3.5-turbo-16k-0613",
|
||||||
|
"gpt-3.5-turbo-16k",
|
||||||
|
"gpt-4-0613",
|
||||||
|
"gpt-4-32k-0613",
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-4-32k",
|
||||||
|
]
|
||||||
|
|
||||||
ANTHROPIC_MODELS = [
|
ANTHROPIC_MODELS = [
|
||||||
"claude-v1", # largest model, ideal for a wide range of more complex tasks.
|
"claude-v1", # largest model, ideal for a wide range of more complex tasks.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import ast
|
import ast
|
||||||
|
import contextlib
|
||||||
import importlib
|
import importlib
|
||||||
import types
|
import types
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
@ -147,11 +148,8 @@ def create_function(code, function_name):
|
||||||
code_obj = compile(
|
code_obj = compile(
|
||||||
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
|
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
|
||||||
)
|
)
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
exec(code_obj, exec_globals, locals())
|
exec(code_obj, exec_globals, locals())
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
exec_globals[function_name] = locals()[function_name]
|
exec_globals[function_name] = locals()[function_name]
|
||||||
|
|
||||||
# Return a function that imports necessary modules and calls the target function
|
# Return a function that imports necessary modules and calls the target function
|
||||||
|
|
|
||||||
1
src/frontend/.githooks/prepare-commit-msg
Symbolic link
1
src/frontend/.githooks/prepare-commit-msg
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/lib/node_modules/opencommit/out/cli.cjs
|
||||||
256
src/frontend/package-lock.json
generated
256
src/frontend/package-lock.json
generated
|
|
@ -15,9 +15,13 @@
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-menubar": "^1.0.3",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tabler/icons-react": "^2.18.0",
|
"@tabler/icons-react": "^2.18.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
|
@ -49,6 +53,7 @@
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"shadcn-ui": "^0.1.3",
|
"shadcn-ui": "^0.1.3",
|
||||||
|
"short-unique-id": "^4.4.4",
|
||||||
"switch": "^0.0.0",
|
"switch": "^0.0.0",
|
||||||
"table": "^6.8.1",
|
"table": "^6.8.1",
|
||||||
"tailwind-merge": "^1.13.0",
|
"tailwind-merge": "^1.13.0",
|
||||||
|
|
@ -1310,6 +1315,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-slot": "1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1377,6 +1408,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1403,6 +1451,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-menu": "2.0.5",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-focus-guards": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1482,6 +1559,78 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.0.4",
|
||||||
|
"@radix-ui/react-focus-guards": "1.0.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.0.3",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-popper": "1.1.2",
|
||||||
|
"@radix-ui/react-portal": "1.0.3",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.4",
|
||||||
|
"@radix-ui/react-slot": "1.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.5.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menubar": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-GqjdxzYCjjKhcgEODDP8SrYfbWNh/Hm3lyuFkP5Q5IbX0QfXklLF1o1AqA3oTV2kulUgN/kOZVS92hIIShEgpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-menu": "2.0.5",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.4",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1580,6 +1729,60 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-separator": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -1625,6 +1828,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.4",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -3199,7 +3432,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -7639,7 +7873,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.5",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-remove-scroll-bar": "^2.3.3",
|
"react-remove-scroll-bar": "^2.3.3",
|
||||||
"react-style-singleton": "^2.2.1",
|
"react-style-singleton": "^2.2.1",
|
||||||
|
|
@ -7662,7 +7897,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/react-remove-scroll-bar": {
|
"node_modules/react-remove-scroll-bar": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-style-singleton": "^2.2.1",
|
"react-style-singleton": "^2.2.1",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
|
|
@ -8407,6 +8643,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/short-unique-id": {
|
||||||
|
"version": "4.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-4.4.4.tgz",
|
||||||
|
"integrity": "sha512-oLF1NCmtbiTWl2SqdXZQbo5KM1b7axdp0RgQLq8qCBBLoq+o3A5wmLrNM6bZIh54/a8BJ3l69kTXuxwZ+XCYuw==",
|
||||||
|
"bin": {
|
||||||
|
"short-unique-id": "bin/short-unique-id",
|
||||||
|
"suid": "bin/short-unique-id"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -9018,8 +9263,9 @@
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.5.3",
|
"version": "2.5.2",
|
||||||
"license": "0BSD"
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
|
||||||
|
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA=="
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,14 @@
|
||||||
"@headlessui/react": "^1.7.10",
|
"@headlessui/react": "^1.7.10",
|
||||||
"@heroicons/react": "^2.0.15",
|
"@heroicons/react": "^2.0.15",
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
|
"@radix-ui/react-menubar": "^1.0.3",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tabler/icons-react": "^2.18.0",
|
"@tabler/icons-react": "^2.18.0",
|
||||||
|
|
@ -44,6 +48,7 @@
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"shadcn-ui": "^0.1.3",
|
"shadcn-ui": "^0.1.3",
|
||||||
|
"short-unique-id": "^4.4.4",
|
||||||
"switch": "^0.0.0",
|
"switch": "^0.0.0",
|
||||||
"table": "^6.8.1",
|
"table": "^6.8.1",
|
||||||
"tailwind-merge": "^1.13.0",
|
"tailwind-merge": "^1.13.0",
|
||||||
|
|
@ -57,7 +62,7 @@
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\""
|
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ module.exports = {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ import _ from "lodash";
|
||||||
import ErrorAlert from "./alerts/error";
|
import ErrorAlert from "./alerts/error";
|
||||||
import NoticeAlert from "./alerts/notice";
|
import NoticeAlert from "./alerts/notice";
|
||||||
import SuccessAlert from "./alerts/success";
|
import SuccessAlert from "./alerts/success";
|
||||||
import ExtraSidebar from "./components/ExtraSidebarComponent";
|
|
||||||
import { alertContext } from "./contexts/alertContext";
|
import { alertContext } from "./contexts/alertContext";
|
||||||
import { locationContext } from "./contexts/locationContext";
|
import { locationContext } from "./contexts/locationContext";
|
||||||
import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent";
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||||
import { TabsContext } from "./contexts/tabsContext";
|
import { TabsContext } from "./contexts/tabsContext";
|
||||||
import { getVersion } from "./controllers/API";
|
import { getVersion } from "./controllers/API";
|
||||||
|
import Router from "./routes";
|
||||||
|
import Header from "./components/headerComponent";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||||
|
|
@ -47,13 +47,6 @@ export default function App() {
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
// Initialize state variable for the version
|
|
||||||
const [version, setVersion] = useState("");
|
|
||||||
useEffect(() => {
|
|
||||||
getVersion().then((response) => {
|
|
||||||
setVersion(response.data.version);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
// Use effect hook to update alertsList when a new alert is added
|
// Use effect hook to update alertsList when a new alert is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If there is an error alert open with data, add it to the alertsList
|
// If there is an error alert open with data, add it to the alertsList
|
||||||
|
|
@ -111,7 +104,6 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
//need parent component with width and height
|
//need parent component with width and height
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex grow-0 shrink basis-auto"></div>
|
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
onReset={() => {
|
onReset={() => {
|
||||||
window.localStorage.removeItem("tabsData");
|
window.localStorage.removeItem("tabsData");
|
||||||
|
|
@ -121,16 +113,8 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
FallbackComponent={CrashErrorComponent}
|
FallbackComponent={CrashErrorComponent}
|
||||||
>
|
>
|
||||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
<Header />
|
||||||
<ExtraSidebar />
|
<Router />
|
||||||
{/* Main area */}
|
|
||||||
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
|
|
||||||
{/* Primary column */}
|
|
||||||
<div className="w-full h-full">
|
|
||||||
<TabsManagerComponent></TabsManagerComponent>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -166,14 +150,6 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
target={"_blank"}
|
|
||||||
href="https://logspace.ai/"
|
|
||||||
className="absolute left-7 bottom-2 flex h-6 cursor-pointer flex-col items-center justify-start overflow-hidden rounded-lg bg-gray-800 px-2 text-center font-sans text-xs tracking-wide text-gray-300 transition-all duration-500 ease-in-out hover:h-12 dark:bg-gray-100 dark:text-gray-800"
|
|
||||||
>
|
|
||||||
{version && <div className="mt-1">⛓️ LangFlow v{version}</div>}
|
|
||||||
<div className={version ? "mt-2" : "mt-1"}>Created by Logspace</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||||
import Tooltip from "../../../../components/TooltipComponent";
|
|
||||||
import {
|
import {
|
||||||
classNames,
|
classNames,
|
||||||
groupByFamily,
|
groupByFamily,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
toFirstUpperCase,
|
|
||||||
} from "../../../../utils";
|
} from "../../../../utils";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import InputComponent from "../../../../components/inputComponent";
|
import InputComponent from "../../../../components/inputComponent";
|
||||||
import ToggleComponent from "../../../../components/toggleComponent";
|
|
||||||
import InputListComponent from "../../../../components/inputListComponent";
|
import InputListComponent from "../../../../components/inputListComponent";
|
||||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||||
import { typesContext } from "../../../../contexts/typesContext";
|
import { typesContext } from "../../../../contexts/typesContext";
|
||||||
|
|
@ -24,6 +21,8 @@ import { nodeNames, nodeIcons } from "../../../../utils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { nodeColors } from "../../../../utils";
|
import { nodeColors } from "../../../../utils";
|
||||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||||
|
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||||
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||||
|
|
||||||
export default function ParameterComponent({
|
export default function ParameterComponent({
|
||||||
left,
|
left,
|
||||||
|
|
@ -40,6 +39,9 @@ export default function ParameterComponent({
|
||||||
const refHtml = useRef(null);
|
const refHtml = useRef(null);
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const [position, setPosition] = useState(0);
|
const [position, setPosition] = useState(0);
|
||||||
|
const { closePopUp } = useContext(PopUpContext);
|
||||||
|
const { setTabsState, tabId } = useContext(TabsContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
||||||
|
|
@ -54,12 +56,27 @@ export default function ParameterComponent({
|
||||||
const [enabled, setEnabled] = useState(
|
const [enabled, setEnabled] = useState(
|
||||||
data.node.template[name]?.value ?? false
|
data.node.template[name]?.value ?? false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||||
|
|
||||||
const { reactFlowInstance } = useContext(typesContext);
|
const { reactFlowInstance } = useContext(typesContext);
|
||||||
let disabled =
|
let disabled =
|
||||||
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||||
const { save } = useContext(TabsContext);
|
|
||||||
const [myData, setMyData] = useState(useContext(typesContext).data);
|
const [myData, setMyData] = useState(useContext(typesContext).data);
|
||||||
|
|
||||||
|
const handleOnNewValue = (newValue: any) => {
|
||||||
|
data.node.template[name].value = newValue;
|
||||||
|
// Set state to pending
|
||||||
|
setTabsState((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[tabId]: {
|
||||||
|
isPending: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const groupedObj = groupByFamily(myData, tooltipTitle);
|
const groupedObj = groupByFamily(myData, tooltipTitle);
|
||||||
|
|
||||||
|
|
@ -104,7 +121,7 @@ export default function ParameterComponent({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="w-full flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
className="w-full flex flex-wrap justify-between items-center bg-muted dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className={"text-sm truncate w-full " + (left ? "" : "text-end")}>
|
<div className={"text-sm truncate w-full " + (left ? "" : "text-end")}>
|
||||||
|
|
@ -125,6 +142,7 @@ export default function ParameterComponent({
|
||||||
delayDuration={0}
|
delayDuration={0}
|
||||||
content={refHtml.current}
|
content={refHtml.current}
|
||||||
side={left ? "left" : "right"}
|
side={left ? "left" : "right"}
|
||||||
|
open={refHtml?.current?.length > 0}
|
||||||
>
|
>
|
||||||
<Handle
|
<Handle
|
||||||
type={left ? "target" : "source"}
|
type={left ? "target" : "source"}
|
||||||
|
|
@ -158,19 +176,13 @@ export default function ParameterComponent({
|
||||||
? [""]
|
? [""]
|
||||||
: data.node.template[name].value
|
: data.node.template[name].value
|
||||||
}
|
}
|
||||||
onChange={(t: string[]) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : data.node.template[name].multiline ? (
|
) : data.node.template[name].multiline ? (
|
||||||
<TextAreaComponent
|
<TextAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InputComponent
|
<InputComponent
|
||||||
|
|
@ -178,84 +190,72 @@ export default function ParameterComponent({
|
||||||
disableCopyPaste={true}
|
disableCopyPaste={true}
|
||||||
password={data.node.template[name].password ?? false}
|
password={data.node.template[name].password ?? false}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "bool" ? (
|
) : left === true && type === "bool" ? (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<ToggleComponent
|
<ToggleShadComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
setEnabled={(t) => {
|
setEnabled={(t) => {
|
||||||
data.node.template[name].value = t;
|
handleOnNewValue(t);
|
||||||
setEnabled(t);
|
setEnabled(t);
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "float" ? (
|
) : left === true && type === "float" ? (
|
||||||
|
<div className="mt-2 w-full">
|
||||||
<FloatComponent
|
<FloatComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
disableCopyPaste={true}
|
disableCopyPaste={true}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : left === true &&
|
) : left === true &&
|
||||||
type === "str" &&
|
type === "str" &&
|
||||||
data.node.template[name].options ? (
|
data.node.template[name].options ? (
|
||||||
|
<div className="w-full">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={data.node.template[name].options}
|
options={data.node.template[name].options}
|
||||||
onSelect={(newValue) => (data.node.template[name].value = newValue)}
|
onSelect={handleOnNewValue}
|
||||||
value={data.node.template[name].value ?? "Choose an option"}
|
value={data.node.template[name].value ?? "Choose an option"}
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
|
</div>
|
||||||
) : left === true && type === "code" ? (
|
) : left === true && type === "code" ? (
|
||||||
<CodeAreaComponent
|
<CodeAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : left === true && type === "file" ? (
|
) : left === true && type === "file" ? (
|
||||||
<InputFileComponent
|
<InputFileComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
}}
|
|
||||||
fileTypes={data.node.template[name].fileTypes}
|
fileTypes={data.node.template[name].fileTypes}
|
||||||
suffixes={data.node.template[name].suffixes}
|
suffixes={data.node.template[name].suffixes}
|
||||||
onFileChange={(t: string) => {
|
onFileChange={(t: string) => {
|
||||||
data.node.template[name].content = t;
|
data.node.template[name].content = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
></InputFileComponent>
|
></InputFileComponent>
|
||||||
) : left === true && type === "int" ? (
|
) : left === true && type === "int" ? (
|
||||||
|
<div className="mt-2 w-full">
|
||||||
<IntComponent
|
<IntComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
disableCopyPaste={true}
|
disableCopyPaste={true}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : left === true && type === "prompt" ? (
|
) : left === true && type === "prompt" ? (
|
||||||
<PromptAreaComponent
|
<PromptAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,18 @@
|
||||||
import {
|
import { classNames, nodeColors, nodeIcons, toTitleCase } from "../../utils";
|
||||||
BugAntIcon,
|
|
||||||
Cog6ToothIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CheckCircleIcon,
|
|
||||||
EllipsisHorizontalCircleIcon,
|
|
||||||
ExclamationCircleIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
import {
|
|
||||||
classNames,
|
|
||||||
nodeColors,
|
|
||||||
nodeIcons,
|
|
||||||
toNormalCase,
|
|
||||||
toTitleCase,
|
|
||||||
} from "../../utils";
|
|
||||||
import ParameterComponent from "./components/parameterComponent";
|
import ParameterComponent from "./components/parameterComponent";
|
||||||
import { typesContext } from "../../contexts/typesContext";
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
import { useContext, useState, useEffect, useRef, Fragment } from "react";
|
import { useContext, useState, useEffect, useRef } from "react";
|
||||||
import { NodeDataType } from "../../types/flow";
|
import { NodeDataType } from "../../types/flow";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { PopUpContext } from "../../contexts/popUpContext";
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import NodeModal from "../../modals/NodeModal";
|
import NodeModal from "../../modals/NodeModal";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
|
||||||
import { debounce } from "../../utils";
|
|
||||||
import Tooltip from "../../components/TooltipComponent";
|
import Tooltip from "../../components/TooltipComponent";
|
||||||
import { NodeToolbar } from "reactflow";
|
import { NodeToolbar } from "reactflow";
|
||||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||||
|
|
||||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||||
import { postValidateNode } from "../../controllers/API";
|
import { useSSE } from "../../contexts/SSEContext";
|
||||||
|
|
||||||
export default function GenericNode({
|
export default function GenericNode({
|
||||||
data,
|
data,
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -44,46 +23,30 @@ export default function GenericNode({
|
||||||
const { setErrorData } = useContext(alertContext);
|
const { setErrorData } = useContext(alertContext);
|
||||||
const showError = useRef(true);
|
const showError = useRef(true);
|
||||||
const { types, deleteNode } = useContext(typesContext);
|
const { types, deleteNode } = useContext(typesContext);
|
||||||
const { openPopUp } = useContext(PopUpContext);
|
|
||||||
|
const { closePopUp, openPopUp } = useContext(PopUpContext);
|
||||||
|
|
||||||
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
|
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
|
||||||
const [validationStatus, setValidationStatus] = useState(null);
|
const [validationStatus, setValidationStatus] = useState(null);
|
||||||
// State for outline color
|
// State for outline color
|
||||||
const [isValid, setIsValid] = useState(false);
|
const { sseData, isBuilding } = useSSE();
|
||||||
const { save } = useContext(TabsContext);
|
|
||||||
const { reactFlowInstance } = useContext(typesContext);
|
|
||||||
const [params, setParams] = useState([]);
|
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (reactFlowInstance) {
|
||||||
|
// setParams(Object.values(reactFlowInstance.toObject()));
|
||||||
|
// }
|
||||||
|
// }, [save]);
|
||||||
|
|
||||||
|
// New useEffect to watch for changes in sseData and update validation status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (reactFlowInstance) {
|
const relevantData = sseData[data.id];
|
||||||
setParams(Object.values(reactFlowInstance.toObject()));
|
if (relevantData) {
|
||||||
|
// Extract validation information from relevantData and update the validationStatus state
|
||||||
|
setValidationStatus(relevantData);
|
||||||
|
} else {
|
||||||
|
setValidationStatus(null);
|
||||||
}
|
}
|
||||||
}, [save]);
|
}, [sseData, data.id]);
|
||||||
|
|
||||||
const validateNode = useCallback(
|
|
||||||
debounce(async () => {
|
|
||||||
try {
|
|
||||||
const response = await postValidateNode(
|
|
||||||
data.id,
|
|
||||||
reactFlowInstance.toObject()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
let jsonResponseParsed = await JSON.parse(response.data);
|
|
||||||
setValidationStatus(jsonResponseParsed);
|
|
||||||
}
|
|
||||||
} 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]);
|
|
||||||
|
|
||||||
if (!Icon) {
|
if (!Icon) {
|
||||||
if (showError.current) {
|
if (showError.current) {
|
||||||
|
|
@ -98,6 +61,8 @@ export default function GenericNode({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NodeToolbar>
|
<NodeToolbar>
|
||||||
|
|
@ -110,11 +75,11 @@ export default function GenericNode({
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
selected ? "border border-blue-500" : "border dark:border-gray-700",
|
selected ? "border border-ring" : "border dark:border-gray-700",
|
||||||
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
|
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-gray-50 p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
|
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-muted p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
|
||||||
<div className="flex w-full items-center gap-2 truncate text-lg">
|
<div className="flex w-full items-center gap-2 truncate text-lg">
|
||||||
<Icon
|
<Icon
|
||||||
className="h-10 w-10 rounded p-1"
|
className="h-10 w-10 rounded p-1"
|
||||||
|
|
@ -124,7 +89,7 @@ export default function GenericNode({
|
||||||
/>
|
/>
|
||||||
<div className="ml-2 truncate">
|
<div className="ml-2 truncate">
|
||||||
<ShadTooltip delayDuration={1500} content={data.type}>
|
<ShadTooltip delayDuration={1500} content={data.type}>
|
||||||
<div className="ml-2 truncate">{data.type}</div>
|
<div className="ml-2 truncate text-gray-800">{data.type}</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -172,7 +137,7 @@ export default function GenericNode({
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
!validationStatus
|
!validationStatus || isBuilding
|
||||||
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
|
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
|
||||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||||
|
|
@ -184,8 +149,8 @@ export default function GenericNode({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full py-5">
|
<div className="h-full w-full py-5 text-gray-800">
|
||||||
<div className="w-full px-5 pb-3 text-sm text-gray-500 dark:text-gray-300">
|
<div className="w-full px-5 pb-3 text-sm text-muted-foreground">
|
||||||
{data.node.description}
|
{data.node.description}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default function SingleAlert({
|
||||||
{dropItem.link ? (
|
{dropItem.link ? (
|
||||||
<Link
|
<Link
|
||||||
to={dropItem.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"
|
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-ring"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default function NoticeAlert({
|
||||||
{link !== "" ? (
|
{link !== "" ? (
|
||||||
<Link
|
<Link
|
||||||
to={link}
|
to={link}
|
||||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-blue-600"
|
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-ring"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import React, { useState, ChangeEvent } from "react";
|
||||||
|
import { Textarea } from "../../components/ui/textarea";
|
||||||
|
import { Label } from "../../components/ui/label";
|
||||||
|
import { Input } from "../../components/ui/input";
|
||||||
|
|
||||||
|
type InputProps = {
|
||||||
|
name: string | null;
|
||||||
|
description: string | null;
|
||||||
|
maxLength?: number;
|
||||||
|
flows: Array<{ id: string; name: string }>;
|
||||||
|
tabId: string;
|
||||||
|
setName: (name: string) => void;
|
||||||
|
setDescription: (description: string) => void;
|
||||||
|
updateFlow: (flow: { id: string; name: string }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditFlowSettings: React.FC<InputProps> = ({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
maxLength = 50,
|
||||||
|
flows,
|
||||||
|
tabId,
|
||||||
|
setName,
|
||||||
|
setDescription,
|
||||||
|
updateFlow,
|
||||||
|
}) => {
|
||||||
|
const [isMaxLength, setIsMaxLength] = useState(false);
|
||||||
|
|
||||||
|
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
if (value.length >= maxLength) {
|
||||||
|
setIsMaxLength(true);
|
||||||
|
} else {
|
||||||
|
setIsMaxLength(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setDescription(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">Name</span>{" "}
|
||||||
|
{isMaxLength && (
|
||||||
|
<span className="text-red-500 animate-pulse ml-10">
|
||||||
|
Character limit reached
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className="mt-2 font-normal"
|
||||||
|
onChange={handleNameChange}
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={name ?? ""}
|
||||||
|
placeholder="File name"
|
||||||
|
id="name"
|
||||||
|
maxLength={maxLength}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
<Label>
|
||||||
|
<span className="font-medium">Description (optional)</span>
|
||||||
|
<Textarea
|
||||||
|
name="description"
|
||||||
|
id="description"
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
value={description ?? ""}
|
||||||
|
placeholder="Flow description"
|
||||||
|
className="max-h-[100px] mt-2 font-normal"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFlowSettings;
|
||||||
|
|
@ -21,10 +21,7 @@ export default function ExtraSidebar() {
|
||||||
isStackedOpen ? "w-52" : "w-0 "
|
isStackedOpen ? "w-52" : "w-0 "
|
||||||
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
|
} 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="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start bg-white">
|
||||||
<div className="flex px-4 justify-between align-middle w-full">
|
|
||||||
<span className="text-gray-900 dark:text-white py-[2px] font-medium "></span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-grow flex-col w-full">
|
<div className="flex flex-grow flex-col w-full">
|
||||||
{extraNavigation.options ? (
|
{extraNavigation.options ? (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|
@ -36,8 +33,8 @@ export default function ExtraSidebar() {
|
||||||
to={item.href}
|
to={item.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.href.split("/")[2] === current[4]
|
item.href.split("/")[2] === current[4]
|
||||||
? "bg-gray-100 text-gray-900"
|
? "bg-muted text-gray-900"
|
||||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
: "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
|
||||||
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
|
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -63,9 +60,9 @@ export default function ExtraSidebar() {
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.href.split("/")[2] === current[4]
|
item.href.split("/")[2] === current[4]
|
||||||
? "bg-gray-100 text-gray-900"
|
? "bg-muted text-gray-900"
|
||||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
: "bg-white text-gray-600 hover:bg-muted 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"
|
"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-1 focus:ring-indigo-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
|
|
@ -96,8 +93,8 @@ export default function ExtraSidebar() {
|
||||||
to={subItem.href}
|
to={subItem.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
subItem.href.split("/")[3] === current[5]
|
subItem.href.split("/")[3] === current[5]
|
||||||
? "bg-gray-100 text-gray-900"
|
? "bg-muted text-gray-900"
|
||||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
: "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
|
||||||
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
|
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const ShadTooltip = (props) => {
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={props.delayDuration}>
|
<Tooltip delayDuration={props.delayDuration}>
|
||||||
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
|
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side={props.side}
|
side={props.side}
|
||||||
avoidCollisions={false}
|
avoidCollisions={false}
|
||||||
|
|
|
||||||
71
src/frontend/src/components/cardComponent/index.tsx
Normal file
71
src/frontend/src/components/cardComponent/index.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Trash2, ExternalLink } from "lucide-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
|
import { FlowType } from "../../types/flow";
|
||||||
|
import { gradients } from "../../utils";
|
||||||
|
import {
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
} from "../ui/card";
|
||||||
|
|
||||||
|
export const CardComponent = ({
|
||||||
|
flow,
|
||||||
|
id,
|
||||||
|
onDelete,
|
||||||
|
button,
|
||||||
|
}: {
|
||||||
|
flow: FlowType;
|
||||||
|
id: string;
|
||||||
|
onDelete?: () => void;
|
||||||
|
button?: JSX.Element;
|
||||||
|
}) => {
|
||||||
|
const { removeFlow } = useContext(TabsContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="group">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex w-full items-center gap-4">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"rounded-full w-7 h-7 flex items-center justify-center text-2xl " +
|
||||||
|
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length]
|
||||||
|
}
|
||||||
|
></span>
|
||||||
|
<span className="flex-1 w-full inline-block truncate-doubleline break-words">
|
||||||
|
{flow.name}
|
||||||
|
</span>
|
||||||
|
{onDelete && (
|
||||||
|
<button className="flex self-start" onClick={onDelete}>
|
||||||
|
<Trash2 className="w-4 h-4 text-primary opacity-0 group-hover:opacity-100 transition-all" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="pt-2 pb-2">
|
||||||
|
<div className="truncate-doubleline">
|
||||||
|
{flow.description}
|
||||||
|
{/* {flow.description} */}
|
||||||
|
</div>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardFooter>
|
||||||
|
<div className="flex gap-2 w-full justify-between items-end">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{/* <Badge variant="secondary">Agent</Badge>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<div className="w-3">
|
||||||
|
<OpenAiIcon />
|
||||||
|
</div>
|
||||||
|
<span className="text-base"> </span>OpenAI+
|
||||||
|
</Badge> */}
|
||||||
|
</div>
|
||||||
|
{button && button}
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
159
src/frontend/src/components/chatComponent/buildTrigger/index.tsx
Normal file
159
src/frontend/src/components/chatComponent/buildTrigger/index.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { useState, useContext } from "react";
|
||||||
|
import { Transition } from "@headlessui/react";
|
||||||
|
import { Zap } from "lucide-react";
|
||||||
|
import { validateNodes } from "../../../utils";
|
||||||
|
import { FlowType } from "../../../types/flow";
|
||||||
|
import Loading from "../../../components/ui/loading";
|
||||||
|
import { useSSE } from "../../../contexts/SSEContext";
|
||||||
|
import { typesContext } from "../../../contexts/typesContext";
|
||||||
|
import { alertContext } from "../../../contexts/alertContext";
|
||||||
|
import { postBuildInit } from "../../../controllers/API";
|
||||||
|
|
||||||
|
export default function BuildTrigger({
|
||||||
|
open,
|
||||||
|
flow,
|
||||||
|
setIsBuilt,
|
||||||
|
isBuilt,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
flow: FlowType;
|
||||||
|
setIsBuilt: any;
|
||||||
|
isBuilt: boolean;
|
||||||
|
}) {
|
||||||
|
const { updateSSEData, isBuilding, setIsBuilding } = useSSE();
|
||||||
|
const { reactFlowInstance } = useContext(typesContext);
|
||||||
|
const { setErrorData } = useContext(alertContext);
|
||||||
|
|
||||||
|
async function handleBuild(flow: FlowType) {
|
||||||
|
try {
|
||||||
|
if (isBuilding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const errors = validateNodes(reactFlowInstance);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
setErrorData({
|
||||||
|
title: "Oops! Looks like you missed something",
|
||||||
|
list: errors,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const minimumLoadingTime = 200; // in milliseconds
|
||||||
|
const startTime = Date.now();
|
||||||
|
setIsBuilding(true);
|
||||||
|
|
||||||
|
const allNodesValid = await streamNodeData(flow);
|
||||||
|
await enforceMinimumLoadingTime(startTime, minimumLoadingTime);
|
||||||
|
setIsBuilt(allNodesValid);
|
||||||
|
if (!allNodesValid) {
|
||||||
|
setErrorData({
|
||||||
|
title: "Oops! Looks like you missed something",
|
||||||
|
list: [
|
||||||
|
"Check components and retry. Hover over component status icon 🔴 to inspect.",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
} finally {
|
||||||
|
setIsBuilding(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function streamNodeData(flow: FlowType) {
|
||||||
|
// Step 1: Make a POST request to send the flow data and receive a unique session ID
|
||||||
|
const response = await postBuildInit(flow);
|
||||||
|
const { flowId } = response.data;
|
||||||
|
|
||||||
|
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||||
|
let validationResults = [];
|
||||||
|
let finished = false;
|
||||||
|
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||||
|
const eventSource = new EventSource(apiUrl);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
// If the event is parseable, return
|
||||||
|
if (!event.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsedData = JSON.parse(event.data);
|
||||||
|
// if the event is the end of the stream, close the connection
|
||||||
|
if (parsedData.end_of_stream) {
|
||||||
|
eventSource.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise, process the data
|
||||||
|
const isValid = processStreamResult(parsedData);
|
||||||
|
validationResults.push(isValid);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error("EventSource failed:", error);
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
// Step 3: Wait for the stream to finish
|
||||||
|
while (!finished) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
finished = validationResults.length === flow.data.nodes.length;
|
||||||
|
}
|
||||||
|
// Step 4: Return true if all nodes are valid, false otherwise
|
||||||
|
return validationResults.every((result) => result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processStreamResult(parsedData) {
|
||||||
|
// Process each chunk of data here
|
||||||
|
// Parse the chunk and update the context
|
||||||
|
try {
|
||||||
|
updateSSEData({ [parsedData.id]: parsedData });
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error parsing stream data: ", err);
|
||||||
|
}
|
||||||
|
return parsedData.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enforceMinimumLoadingTime(
|
||||||
|
startTime: number,
|
||||||
|
minimumLoadingTime: number
|
||||||
|
) {
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
const remainingTime = minimumLoadingTime - elapsedTime;
|
||||||
|
|
||||||
|
if (remainingTime > 0) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, remainingTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}>
|
||||||
|
<div
|
||||||
|
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||||
|
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
handleBuild(flow);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
{isBuilding ? (
|
||||||
|
// Render your loading animation here when isBuilding is true
|
||||||
|
<Loading strokeWidth={1.5} style={{ color: "white" }} />
|
||||||
|
) : (
|
||||||
|
<Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
import {
|
import { MessagesSquare } from "lucide-react";
|
||||||
Bars3CenterLeftIcon,
|
|
||||||
ChatBubbleBottomCenterTextIcon,
|
import { alertContext } from "../../../contexts/alertContext";
|
||||||
} from "@heroicons/react/24/outline";
|
import { useContext } from "react";
|
||||||
import { nodeColors } from "../../../utils";
|
|
||||||
import { PopUpContext } from "../../../contexts/popUpContext";
|
export default function ChatTrigger({ open, setOpen, isBuilt }) {
|
||||||
import { useContext } from "react";
|
const { setErrorData } = useContext(alertContext);
|
||||||
import ChatModal from "../../../modals/chatModal";
|
|
||||||
|
function handleClick() {
|
||||||
|
if (isBuilt) {
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Flow not built",
|
||||||
|
list: ["Please build the flow before chatting"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatTrigger({ open, setOpen }) {
|
|
||||||
const { openPopUp } = useContext(PopUpContext);
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
show={!open}
|
show={!open}
|
||||||
|
|
@ -23,16 +31,16 @@ export default function ChatTrigger({ open, setOpen }) {
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-4 right-3">
|
<div className="absolute bottom-4 right-3">
|
||||||
<div
|
<div
|
||||||
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:border-gray-600 cursor-pointer"
|
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||||
onClick={() => {
|
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||||
setOpen(true);
|
onClick={handleClick}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3">
|
||||||
<ChatBubbleBottomCenterTextIcon
|
<MessagesSquare
|
||||||
className="h-6 w-6 mt-1"
|
className="pth-6 w-6 fill-[#5c8be1] stroke-1 stroke-[#5c8be1]"
|
||||||
style={{ color: "white" }}
|
style={{ color: "white" }}
|
||||||
|
strokeWidth={1.5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useNodes } from "reactflow";
|
||||||
import { ChatMessageType, ChatType } from "../../types/chat";
|
import { ChatType } from "../../types/chat";
|
||||||
import ChatTrigger from "./chatTrigger";
|
import ChatTrigger from "./chatTrigger";
|
||||||
|
import BuildTrigger from "./buildTrigger";
|
||||||
import ChatModal from "../../modals/chatModal";
|
import ChatModal from "../../modals/chatModal";
|
||||||
|
|
||||||
import _ from "lodash";
|
import { getBuildStatus } from "../../controllers/API";
|
||||||
|
import { NodeType } from "../../types/flow";
|
||||||
|
|
||||||
export default function Chat({ flow }: ChatType) {
|
export default function Chat({ flow }: ChatType) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isBuilt, setIsBuilt] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
(event.key === "K" || event.key === "k") &&
|
(event.key === "K" || event.key === "k") &&
|
||||||
(event.metaKey || event.ctrlKey)
|
(event.metaKey || event.ctrlKey) &&
|
||||||
|
isBuilt
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setOpen((oldState) => !oldState);
|
setOpen((oldState) => !oldState);
|
||||||
|
|
@ -22,11 +27,58 @@ export default function Chat({ flow }: ChatType) {
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isBuilt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Define an async function within the useEffect hook
|
||||||
|
const fetchBuildStatus = async () => {
|
||||||
|
const response = await getBuildStatus(flow.id);
|
||||||
|
setIsBuilt(response.built);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the async function
|
||||||
|
fetchBuildStatus();
|
||||||
|
}, [flow]);
|
||||||
|
|
||||||
|
const prevNodesRef = useRef<any[] | undefined>();
|
||||||
|
const nodes = useNodes();
|
||||||
|
useEffect(() => {
|
||||||
|
const prevNodes = prevNodesRef.current;
|
||||||
|
const currentNodes = nodes.map(
|
||||||
|
(node: NodeType) => node.data.node.template.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevNodes &&
|
||||||
|
JSON.stringify(prevNodes) !== JSON.stringify(currentNodes)
|
||||||
|
) {
|
||||||
|
setIsBuilt(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevNodesRef.current = currentNodes;
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isBuilt ? (
|
||||||
|
<div>
|
||||||
|
<BuildTrigger
|
||||||
|
open={open}
|
||||||
|
flow={flow}
|
||||||
|
setIsBuilt={setIsBuilt}
|
||||||
|
isBuilt={isBuilt}
|
||||||
|
/>
|
||||||
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||||
<ChatTrigger open={open} setOpen={setOpen} />
|
<ChatTrigger open={open} setOpen={setOpen} isBuilt={isBuilt} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<BuildTrigger
|
||||||
|
open={open}
|
||||||
|
flow={flow}
|
||||||
|
setIsBuilt={setIsBuilt}
|
||||||
|
isBuilt={isBuilt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import CodeAreaModal from "../../modals/codeAreaModal";
|
import CodeAreaModal from "../../modals/codeAreaModal";
|
||||||
import TextAreaModal from "../../modals/textAreaModal";
|
import TextAreaModal from "../../modals/textAreaModal";
|
||||||
import { TextAreaComponentType } from "../../types/components";
|
import { TextAreaComponentType } from "../../types/components";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function CodeAreaComponent({
|
export default function CodeAreaComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -19,13 +20,18 @@ export default function CodeAreaComponent({
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
}, [disabled, onChange]);
|
}, [disabled, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
|
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="w-full flex items-center gap-3">
|
<div className="w-full flex items-center">
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openPopUp(
|
openPopUp(
|
||||||
|
|
@ -40,12 +46,14 @@ export default function CodeAreaComponent({
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "h-7 truncate placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "truncate cursor-pointer placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 border-1 shadow-sm sm:text-sm" +
|
||||||
: "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" +
|
INPUT_STYLE
|
||||||
|
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||||
|
INPUT_STYLE +
|
||||||
(disabled ? " bg-gray-200" : "")
|
(disabled ? " bg-gray-200" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{myValue !== "" ? myValue : "Text empty"}
|
{myValue !== "" ? myValue : "Type something..."}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -61,7 +69,7 @@ export default function CodeAreaComponent({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!editNode && (
|
{!editNode && (
|
||||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
|
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300 ml-3" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import { ChevronUpDownIcon, CheckIcon } from "@heroicons/react/24/outline";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { DropDownComponentType } from "../../types/components";
|
import { DropDownComponentType } from "../../types/components";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function Dropdown({
|
export default function Dropdown({
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
onSelect,
|
onSelect,
|
||||||
editNode = false,
|
editNode = false,
|
||||||
|
numberOfOptions = 0,
|
||||||
}: DropDownComponentType) {
|
}: DropDownComponentType) {
|
||||||
let [internalValue, setInternalValue] = useState(
|
let [internalValue, setInternalValue] = useState(
|
||||||
value === "" || !value ? "Choose an option" : value
|
value === "" || !value ? "Choose an option" : value
|
||||||
|
|
@ -25,24 +27,20 @@ export default function Dropdown({
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={editNode ? "mt-1" : "relative mt-1"}>
|
||||||
className={
|
|
||||||
editNode ? "relative mt-0 w-full" : "relative mt-1 w-full"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "pr-9 arrow-hide placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "relative pr-8 placeholder:text-center block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md shadow-sm sm:text-sm border-gray-300 border-1" +
|
||||||
: "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"
|
INPUT_STYLE
|
||||||
|
: "ring-1 ring-slate-300 dark:ring-slate-600 w-full py-2 pl-3 pr-10 text-left dark:focus:ring-offset-2 dark:focus:ring-offset-gray-900 dark:focus:ring-1 dark:focus:ring-gray-600 dark:focus-visible:ring-gray-900 dark:focus-visible:ring-offset-2 focus-visible:outline-none dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||||
|
INPUT_STYLE
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="block truncate w-full">{internalValue}</span>
|
<span className="block truncate w-full">{internalValue}</span>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
editNode
|
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||||
? "hidden"
|
|
||||||
: "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ChevronUpDownIcon
|
<ChevronUpDownIcon
|
||||||
|
|
@ -62,8 +60,8 @@ export default function Dropdown({
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "arrow-hide"
|
? "absolute z-10 mt-1 max-h-60 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 w-[215px]"
|
||||||
: "absolute z-50 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"
|
: "nowheel absolute z-10 mt-1 max-h-60 w-full overflow-auto overflow-y 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) => (
|
{options.map((option, id) => (
|
||||||
|
|
@ -71,11 +69,8 @@ export default function Dropdown({
|
||||||
key={id}
|
key={id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active && !editNode
|
active
|
||||||
? "text-white bg-indigo-600 dark:bg-indigo-500"
|
? " bg-accent dark:bg-white dark:text-gray-500"
|
||||||
: "text-gray-900",
|
|
||||||
active && editNode
|
|
||||||
? "text-white bg-gray-400 dark:bg-gray-500"
|
|
||||||
: "",
|
: "",
|
||||||
editNode
|
editNode
|
||||||
? "relative cursor-default select-none py-0.5 pl-3 pr-12 dark:text-gray-300 dark:bg-gray-800"
|
? "relative cursor-default select-none py-0.5 pl-3 pr-12 dark:text-gray-300 dark:bg-gray-800"
|
||||||
|
|
@ -98,13 +93,16 @@ export default function Dropdown({
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
editNode ? "text-gray-600" : "text-indigo-600",
|
active ? "text-white dark:text-black" : "",
|
||||||
active ? "text-white" : "",
|
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className="h-5 w-5"
|
className={
|
||||||
|
active
|
||||||
|
? "h-5 w-5 dark:text-black text-black"
|
||||||
|
: "h-5 w-5 dark:text-white text-black"
|
||||||
|
}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { FloatComponentType } from "../../types/components";
|
import { FloatComponentType } from "../../types/components";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function FloatComponent({
|
export default function FloatComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -12,12 +13,21 @@ export default function FloatComponent({
|
||||||
const [myValue, setMyValue] = useState(value ?? "");
|
const [myValue, setMyValue] = useState(value ?? "");
|
||||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||||
|
|
||||||
|
const step = 0.1;
|
||||||
|
const min = 0;
|
||||||
|
const max = 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
setMyValue("");
|
setMyValue("");
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
}, [disabled, onChange]);
|
}, [disabled, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|
@ -32,11 +42,24 @@ export default function FloatComponent({
|
||||||
if (disableCopyPaste) setDisableCopyPaste(false);
|
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||||
}}
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
|
step={step}
|
||||||
|
min={min}
|
||||||
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.value < min.toString()) {
|
||||||
|
e.target.value = min.toString();
|
||||||
|
}
|
||||||
|
if (e.target.value > max.toString()) {
|
||||||
|
e.target.value = max.toString();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
max={max}
|
||||||
value={myValue}
|
value={myValue}
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "text-center arrow-hide placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "focus:placeholder-transparent text-center placeholder:text-center border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||||
: "block w-full form-input dark:bg-gray-900 arrow-hide dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
INPUT_STYLE
|
||||||
|
: "focus:placeholder-transparent block w-full form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm ring-offset-gray-200 sm:text-sm" +
|
||||||
|
INPUT_STYLE +
|
||||||
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||||
|
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
Settings2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "../../../ui/dropdown-menu";
|
||||||
|
|
||||||
|
import { alertContext } from "../../../../contexts/alertContext";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||||
|
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||||
|
import { Button } from "../../../ui/button";
|
||||||
|
|
||||||
|
export const MenuBar = ({ flows, tabId }) => {
|
||||||
|
const { updateFlow, setTabId, addFlow } = useContext(TabsContext);
|
||||||
|
const { setErrorData } = useContext(alertContext);
|
||||||
|
const { openPopUp } = useContext(PopUpContext);
|
||||||
|
const { undo, redo } = useContext(undoRedoContext);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function handleAddFlow() {
|
||||||
|
try {
|
||||||
|
addFlow(null, true).then((id) => {
|
||||||
|
navigate("/flow/" + id);
|
||||||
|
});
|
||||||
|
// saveFlowStyleInDataBase();
|
||||||
|
} catch (err) {
|
||||||
|
setErrorData(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let current_flow = flows.find((flow) => flow.id === tabId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Link to="/">
|
||||||
|
<ChevronLeft className="w-4" />
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center font-medium text-sm rounded-md py-1 px-1.5 gap-0.5">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button
|
||||||
|
className="gap-2 flex items-center max-w-[200px]"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<div className="truncate flex-1">{current_flow.name}</div>
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-44">
|
||||||
|
<DropdownMenuLabel>Options</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleAddFlow();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
New
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
openPopUp(<FlowSettingsModal />);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings2 className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
undo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Undo className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||||
|
Undo
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
redo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Redo className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||||
|
Redo
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{/* <DropdownMenuLabel>Projects</DropdownMenuLabel> */}
|
||||||
|
{/* <DropdownMenuRadioGroup className="max-h-full overflow-scroll"
|
||||||
|
value={tabId}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setTabId(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flows.map((flow, idx) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={"/flow/" + flow.id}
|
||||||
|
className="flex w-full items-center"
|
||||||
|
>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={flow.id}
|
||||||
|
className="flex-1 w-full inline-block truncate break-words mr-2"
|
||||||
|
>
|
||||||
|
{flow.name}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuRadioGroup> */}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuBar;
|
||||||
123
src/frontend/src/components/headerComponent/index.tsx
Normal file
123
src/frontend/src/components/headerComponent/index.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { BellIcon, Home, Users2 } from "lucide-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { FaGithub } from "react-icons/fa";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
|
import AlertDropdown from "../../alerts/alertDropDown";
|
||||||
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
|
import { darkContext } from "../../contexts/darkContext";
|
||||||
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
|
import MenuBar from "./components/menuBar";
|
||||||
|
import { Link, useLocation, useParams } from "react-router-dom";
|
||||||
|
import { USER_PROJECTS_HEADER } from "../../constants";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const { flows, addFlow, tabId } = useContext(TabsContext);
|
||||||
|
const { openPopUp } = useContext(PopUpContext);
|
||||||
|
const { templates } = useContext(typesContext);
|
||||||
|
const { id } = useParams();
|
||||||
|
const AlertWidth = 384;
|
||||||
|
const { dark, setDark } = useContext(darkContext);
|
||||||
|
const { notificationCenter, setNotificationCenter, setErrorData } =
|
||||||
|
useContext(alertContext);
|
||||||
|
const location = useLocation();
|
||||||
|
return (
|
||||||
|
<div className="w-full h-12 flex justify-between items-center border-b bg-muted">
|
||||||
|
<div className="flex gap-2 justify-start items-center w-96">
|
||||||
|
<Link to="/">
|
||||||
|
<span className="text-2xl ml-4">⛓️</span>
|
||||||
|
</Link>
|
||||||
|
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
|
||||||
|
<MenuBar flows={flows} tabId={tabId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Link to="/">
|
||||||
|
<Button
|
||||||
|
className="gap-2"
|
||||||
|
variant={location.pathname === "/" ? "primary" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Home className="w-4 h-4" />
|
||||||
|
<div className="flex-1">{USER_PROJECTS_HEADER}</div>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/community">
|
||||||
|
<Button
|
||||||
|
className="gap-2"
|
||||||
|
variant={
|
||||||
|
location.pathname === "/community" ? "primary" : "secondary"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Users2 className="w-4 h-4" />
|
||||||
|
<div className="flex-1">Community Examples</div>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end px-2 w-96">
|
||||||
|
<div className="ml-auto mr-2 flex gap-5">
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
className="text-gray-600 dark:text-gray-300 "
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/logspace-ai/langflow"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex"
|
||||||
|
>
|
||||||
|
<FaGithub className="h-5 w-5 mr-2" />
|
||||||
|
Join The Community
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
{/* <button
|
||||||
|
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||||
|
onClick={() => {
|
||||||
|
setDark(!dark);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dark ? (
|
||||||
|
<SunIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<MoonIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
|
||||||
|
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setNotificationCenter(false);
|
||||||
|
const { top, left } = (
|
||||||
|
event.target as Element
|
||||||
|
).getBoundingClientRect();
|
||||||
|
openPopUp(
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="z-10 absolute"
|
||||||
|
style={{ top: top + 34, left: left - AlertWidth }}
|
||||||
|
>
|
||||||
|
<AlertDropdown />
|
||||||
|
</div>
|
||||||
|
<div className="h-screen w-screen fixed top-0 left-0"></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{notificationCenter && (
|
||||||
|
<div className="absolute w-1.5 h-1.5 rounded-full bg-destructive right-[3px]"></div>
|
||||||
|
)}
|
||||||
|
<BellIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
{/* <button>
|
||||||
|
<img
|
||||||
|
src="https://github.com/shadcn.png"
|
||||||
|
className="rounded-full w-8"
|
||||||
|
/>
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import { InputComponentType } from "../../types/components";
|
import { InputComponentType } from "../../types/components";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function InputComponent({
|
export default function InputComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -14,6 +16,8 @@ export default function InputComponent({
|
||||||
const [myValue, setMyValue] = useState(value ?? "");
|
const [myValue, setMyValue] = useState(value ?? "");
|
||||||
const [pwdVisible, setPwdVisible] = useState(false);
|
const [pwdVisible, setPwdVisible] = useState(false);
|
||||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||||
|
const { closePopUp } = useContext(PopUpContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
setMyValue("");
|
setMyValue("");
|
||||||
|
|
@ -21,6 +25,10 @@ export default function InputComponent({
|
||||||
}
|
}
|
||||||
}, [disabled, onChange]);
|
}, [disabled, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value ?? "");
|
||||||
|
}, [closePopUp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|
@ -38,12 +46,13 @@ export default function InputComponent({
|
||||||
if (disableCopyPaste) setDisableCopyPaste(false);
|
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"block w-full pr-12 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm",
|
"block w-full pr-12 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:placeholder-transparent",
|
||||||
disabled ? " bg-gray-200 dark:bg-gray-700" : "",
|
disabled ? " bg-gray-200 dark:bg-gray-700" : "",
|
||||||
password && !pwdVisible && myValue !== "" ? "password" : "",
|
password && !pwdVisible && myValue !== "" ? "password" : "",
|
||||||
editNode
|
editNode
|
||||||
? "placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200 text-center"
|
? "border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm text-center" +
|
||||||
: "focus:border-indigo-500 focus:ring-indigo-500",
|
INPUT_STYLE
|
||||||
|
: "ring-offset-gray-200" + INPUT_STYLE,
|
||||||
password && editNode ? "pr-8" : "pr-3"
|
password && editNode ? "pr-8" : "pr-3"
|
||||||
)}
|
)}
|
||||||
placeholder={password && editNode ? "Key" : "Type something..."}
|
placeholder={password && editNode ? "Key" : "Type something..."}
|
||||||
|
|
@ -71,7 +80,11 @@ export default function InputComponent({
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="w-5 h-5"
|
className={classNames(
|
||||||
|
editNode
|
||||||
|
? "w-5 h-5 absolute bottom-0.5 right-2"
|
||||||
|
: "w-5 h-5 absolute bottom-2 right-3"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
|
@ -86,7 +99,11 @@ export default function InputComponent({
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="w-5 h-5"
|
className={classNames(
|
||||||
|
editNode
|
||||||
|
? "w-5 h-5 absolute bottom-0.5 right-2"
|
||||||
|
: "w-5 h-5 absolute bottom-2 right-3"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { FileComponentType } from "../../types/components";
|
import { FileComponentType } from "../../types/components";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function InputFileComponent({
|
export default function InputFileComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -37,6 +38,10 @@ export default function InputFileComponent({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
|
|
@ -73,8 +78,10 @@ export default function InputFileComponent({
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "truncate placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm border-1" +
|
||||||
: "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" +
|
INPUT_STYLE
|
||||||
|
: "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 sm:text-sm" +
|
||||||
|
INPUT_STYLE +
|
||||||
(disabled ? " bg-gray-200" : "")
|
(disabled ? " bg-gray-200" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -82,7 +89,7 @@ export default function InputFileComponent({
|
||||||
</span>
|
</span>
|
||||||
<button onClick={handleButtonClick}>
|
<button onClick={handleButtonClick}>
|
||||||
{!editNode && (
|
{!editNode && (
|
||||||
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-blue-600" />
|
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-ring" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@ import { InputListComponentType } from "../../types/components";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
export default function InputListComponent({
|
export default function InputListComponent({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
editNode = false,
|
||||||
}: InputListComponentType) {
|
}: InputListComponentType) {
|
||||||
const [inputList, setInputList] = useState(value ?? [""]);
|
const [inputList, setInputList] = useState(value ?? [""]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -21,7 +22,7 @@ export default function InputListComponent({
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
|
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
|
||||||
"flex flex-col gap-3"
|
"flex flex-col gap-3 py-2"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{inputList.map((i, idx) => (
|
{inputList.map((i, idx) => (
|
||||||
|
|
@ -30,8 +31,12 @@ export default function InputListComponent({
|
||||||
type="text"
|
type="text"
|
||||||
value={i}
|
value={i}
|
||||||
className={
|
className={
|
||||||
"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
editNode
|
||||||
(disabled ? " bg-gray-200" : "")
|
? "border-[1px] truncate cursor-pointer text-center placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||||
|
INPUT_STYLE
|
||||||
|
: "block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm" +
|
||||||
|
(disabled ? " bg-gray-200" : "") +
|
||||||
|
"focus:placeholder-transparent"
|
||||||
}
|
}
|
||||||
placeholder="Type something..."
|
placeholder="Type something..."
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -54,7 +59,7 @@ export default function InputListComponent({
|
||||||
onChange(inputList);
|
onChange(inputList);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="w-4 h-4 hover:text-blue-600" />
|
<PlusIcon className={"w-4 h-4 hover:text-ring"} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import { FloatComponentType } from "../../types/components";
|
import { FloatComponentType } from "../../types/components";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function IntComponent({
|
export default function IntComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -12,6 +13,7 @@ export default function IntComponent({
|
||||||
}: FloatComponentType) {
|
}: FloatComponentType) {
|
||||||
const [myValue, setMyValue] = useState(value ?? "");
|
const [myValue, setMyValue] = useState(value ?? "");
|
||||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||||
|
const min = 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
|
|
@ -19,6 +21,11 @@ export default function IntComponent({
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
}, [disabled, onChange]);
|
}, [disabled, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|
@ -53,11 +60,20 @@ export default function IntComponent({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min={min}
|
||||||
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.value < min.toString()) {
|
||||||
|
e.target.value = min.toString();
|
||||||
|
}
|
||||||
|
}}
|
||||||
value={myValue}
|
value={myValue}
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "text-center arrow-hide placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "focus:placeholder-transparent text-center placeholder:text-center border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||||
: "block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 dark:text-gray-300 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
INPUT_STYLE
|
||||||
|
: "focus:placeholder-transparent block w-full form-input dark:bg-gray-900 dark:border-gray-600 dark:text-gray-300 rounded-md border-gray-300 shadow-sm ring-offset-background sm:text-sm" +
|
||||||
|
INPUT_STYLE +
|
||||||
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
||||||
}
|
}
|
||||||
placeholder={editNode ? "Integer number" : "Type a integer number"}
|
placeholder={editNode ? "Integer number" : "Type a integer number"}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default function LoadingComponent({ remSize }: LoadingComponentProps) {
|
||||||
<div role="status" className="w-min m-auto">
|
<div role="status" className="w-min m-auto">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`w-${remSize} h-${remSize} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
|
className={`w-${remSize} h-${remSize} mr-2 text-muted animate-spin dark:text-gray-600 fill-blue-600`}
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { PopUpContext } from "../../contexts/popUpContext";
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import CodeAreaModal from "../../modals/codeAreaModal";
|
|
||||||
import TextAreaModal from "../../modals/textAreaModal";
|
|
||||||
import { TextAreaComponentType } from "../../types/components";
|
import { TextAreaComponentType } from "../../types/components";
|
||||||
import GenericModal from "../../modals/genericModal";
|
import GenericModal from "../../modals/genericModal";
|
||||||
import { TypeModal } from "../../utils";
|
import { TypeModal } from "../../utils";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
|
|
||||||
export default function PromptAreaComponent({
|
export default function PromptAreaComponent({
|
||||||
value,
|
value,
|
||||||
|
|
@ -21,6 +20,11 @@ export default function PromptAreaComponent({
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
}, [disabled, onChange]);
|
}, [disabled, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|
@ -45,12 +49,13 @@ export default function PromptAreaComponent({
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "h-7 truncate placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "cursor-pointer truncate placeholder:text-center text-gray-500 border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||||
: "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" +
|
INPUT_STYLE
|
||||||
|
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||||
(disabled ? " bg-gray-200" : "")
|
(disabled ? " bg-gray-200" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{myValue !== "" ? myValue : "-"}
|
{myValue !== "" ? myValue : "Type your prompt here"}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -69,7 +74,7 @@ export default function PromptAreaComponent({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!editNode && (
|
{!editNode && (
|
||||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
|
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import { TextAreaComponentType } from "../../types/components";
|
import { TextAreaComponentType } from "../../types/components";
|
||||||
import GenericModal from "../../modals/genericModal";
|
import GenericModal from "../../modals/genericModal";
|
||||||
import { TypeModal } from "../../utils";
|
import { TypeModal } from "../../utils";
|
||||||
|
import { INPUT_STYLE } from "../../constants";
|
||||||
export default function TextAreaComponent({
|
export default function TextAreaComponent({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
|
@ -12,16 +12,28 @@ export default function TextAreaComponent({
|
||||||
editNode = false,
|
editNode = false,
|
||||||
}: TextAreaComponentType) {
|
}: TextAreaComponentType) {
|
||||||
const [myValue, setMyValue] = useState(value);
|
const [myValue, setMyValue] = useState(value);
|
||||||
const { openPopUp } = useContext(PopUpContext);
|
const { openPopUp, closePopUp } = useContext(PopUpContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
setMyValue("");
|
setMyValue("");
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
}, [disabled, onChange]);
|
}, [disabled, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value);
|
||||||
|
}, [closePopUp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||||
<div className="w-full flex items-center gap-3">
|
<div
|
||||||
|
className={
|
||||||
|
editNode
|
||||||
|
? "w-full flex items-center"
|
||||||
|
: "w-full flex items-center gap-3"
|
||||||
|
}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openPopUp(
|
openPopUp(
|
||||||
|
|
@ -39,12 +51,13 @@ export default function TextAreaComponent({
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
editNode
|
editNode
|
||||||
? "h-7 truncate placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
? "truncate cursor-pointer placeholder:text-center text-gray-500 border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm 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" +
|
INPUT_STYLE
|
||||||
|
: "truncate block w-full text-gray-500 dark:text-muted px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||||
(disabled ? " bg-gray-200" : "")
|
(disabled ? " bg-gray-200" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{myValue !== "" ? myValue : "Text empty"}
|
{myValue !== "" ? myValue : "Type something..."}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -63,7 +76,7 @@ export default function TextAreaComponent({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!editNode && (
|
{!editNode && (
|
||||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
|
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export default function ToggleComponent({
|
||||||
setEnabled(x);
|
setEnabled(x);
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
enabled ? "bg-indigo-600" : "bg-gray-200",
|
enabled ? "bg-primary" : "bg-gray-200",
|
||||||
"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 focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
|
"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 focus:outline-none focus:ring-1 focus:ring-primary focus:ring-offset-1"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { classNames } from "../../utils";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { ToggleComponentType } from "../../types/components";
|
import { ToggleComponentType } from "../../types/components";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
|
|
@ -7,18 +6,38 @@ export default function ToggleShadComponent({
|
||||||
enabled,
|
enabled,
|
||||||
setEnabled,
|
setEnabled,
|
||||||
disabled,
|
disabled,
|
||||||
|
size,
|
||||||
}: ToggleComponentType) {
|
}: ToggleComponentType) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
}
|
}
|
||||||
}, [disabled, setEnabled]);
|
}, [disabled, setEnabled]);
|
||||||
|
let scaleX, scaleY;
|
||||||
|
switch (size) {
|
||||||
|
case "small":
|
||||||
|
scaleX = 0.6;
|
||||||
|
scaleY = 0.6;
|
||||||
|
break;
|
||||||
|
case "medium":
|
||||||
|
scaleX = 0.8;
|
||||||
|
scaleY = 0.8;
|
||||||
|
break;
|
||||||
|
case "large":
|
||||||
|
scaleX = 1;
|
||||||
|
scaleY = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
scaleX = 1;
|
||||||
|
scaleY = 1;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||||
<Switch
|
<Switch
|
||||||
style={{
|
style={{
|
||||||
transform: "scaleX(0.6) scaleY(0.6)",
|
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||||
}}
|
}}
|
||||||
|
className="data-[state=unchecked]:bg-slate-500"
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onCheckedChange={(x: boolean) => {
|
onCheckedChange={(x: boolean) => {
|
||||||
setEnabled(x);
|
setEnabled(x);
|
||||||
|
|
|
||||||
35
src/frontend/src/components/ui/badge.tsx
Normal file
35
src/frontend/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center border rounded-full px-2.5 h-6 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
|
|
@ -13,8 +13,10 @@ const buttonVariants = cva(
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||||
|
primary:
|
||||||
|
"border bg-background text-secondary-foreground hover:bg-background/80 hover:shadow-sm",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "underline-offset-4 hover:underline text-primary",
|
link: "underline-offset-4 hover:underline text-primary",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
85
src/frontend/src/components/ui/card.tsx
Normal file
85
src/frontend/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg flex flex-col justify-between border bg-card text-card-foreground shadow-sm hover:shadow-lg transition-all",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-base font-semibold leading-tight tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(" flex items-center p-4 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|
@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
"fixed inset-0 z-50 bg-primary/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
199
src/frontend/src/components/ui/dropdown-menu.tsx
Normal file
199
src/frontend/src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 pl-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
};
|
||||||
39
src/frontend/src/components/ui/loading.tsx
Normal file
39
src/frontend/src/components/ui/loading.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
|
// https://github.com/feathericons/feather/issues/695#issuecomment-1503699643
|
||||||
|
export const Loading = (props: SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="feather feather-circle"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle cx={12} cy={12} r={10} strokeDasharray={63} strokeDashoffset={21}>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 12 12"
|
||||||
|
to="360 12 12"
|
||||||
|
dur="2s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
dur="8s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
keyTimes="0; 0.5; 1"
|
||||||
|
values="-16; -47; -16"
|
||||||
|
calcMode="spline"
|
||||||
|
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
export default Loading;
|
||||||
236
src/frontend/src/components/ui/menubar.tsx
Normal file
236
src/frontend/src/components/ui/menubar.tsx
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const MenubarMenu = MenubarPrimitive.Menu;
|
||||||
|
|
||||||
|
const MenubarGroup = MenubarPrimitive.Group;
|
||||||
|
|
||||||
|
const MenubarPortal = MenubarPrimitive.Portal;
|
||||||
|
|
||||||
|
const MenubarSub = MenubarPrimitive.Sub;
|
||||||
|
|
||||||
|
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const Menubar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const MenubarTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const MenubarSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</MenubarPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const MenubarSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const MenubarContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<MenubarPrimitive.Portal>
|
||||||
|
<MenubarPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in slide-in-from-top-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</MenubarPrimitive.Portal>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const MenubarItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const MenubarCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const MenubarRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const MenubarLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const MenubarSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<MenubarPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const MenubarShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenubarShortcut.displayname = "MenubarShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Menubar,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarTrigger,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarLabel,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
MenubarRadioGroup,
|
||||||
|
MenubarRadioItem,
|
||||||
|
MenubarPortal,
|
||||||
|
MenubarSubContent,
|
||||||
|
MenubarSubTrigger,
|
||||||
|
MenubarGroup,
|
||||||
|
MenubarSub,
|
||||||
|
MenubarShortcut,
|
||||||
|
};
|
||||||
89
src/frontend/src/components/ui/rename-label.tsx
Normal file
89
src/frontend/src/components/ui/rename-label.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
export default function RenameLabel(props) {
|
||||||
|
const [internalState, setInternalState] = useState(false);
|
||||||
|
const [isRename, setIsRename] = props.rename
|
||||||
|
? [props.rename, props.setRename]
|
||||||
|
: [internalState, setInternalState];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.value) setMyValue(props.value);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
const [myValue, setMyValue] = useState(props.value);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRename) {
|
||||||
|
setMyValue(props.value);
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setIsRename(false);
|
||||||
|
props.setValue("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (inputRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resizeInput();
|
||||||
|
}, [isRename]);
|
||||||
|
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
const resizeInput = () => {
|
||||||
|
const input = inputRef.current;
|
||||||
|
if (input) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.style.position = "absolute";
|
||||||
|
span.style.visibility = "hidden";
|
||||||
|
span.style.whiteSpace = "pre";
|
||||||
|
span.style.font = window.getComputedStyle(input).font;
|
||||||
|
span.textContent = input.value;
|
||||||
|
|
||||||
|
document.body.appendChild(span);
|
||||||
|
const textWidth = span.getBoundingClientRect().width;
|
||||||
|
document.body.removeChild(span);
|
||||||
|
|
||||||
|
input.style.width = `${textWidth + 16}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isRename ? (
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
ref={inputRef}
|
||||||
|
onInput={resizeInput}
|
||||||
|
className={cn(
|
||||||
|
"px-2 bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
onBlur={() => {
|
||||||
|
setIsRename(false);
|
||||||
|
if (props.value !== "") {
|
||||||
|
props.setValue(myValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={myValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setMyValue(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn("px-2 text-left truncate", props.className)}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
setIsRename(true);
|
||||||
|
setMyValue(props.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/frontend/src/components/ui/separator.tsx
Normal file
30
src/frontend/src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
54
src/frontend/src/components/ui/tabs.tsx
Normal file
54
src/frontend/src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=inactive]:hover:bg-secondary/80 data-[state=active]:border data-[state=inactive]:border data-[state=inactive]:border-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
// src/constants.tsx
|
// src/constants.tsx
|
||||||
|
|
||||||
|
import { FlowType } from "./types/flow";
|
||||||
|
import { buildTweaks } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base text for subtitle of Export Dialog (Toolbar)
|
* The base text for subtitle of Export Dialog (Toolbar)
|
||||||
* @constant
|
* @constant
|
||||||
*/
|
*/
|
||||||
export const EXPORT_DIALOG_SUBTITLE = "Export your models.";
|
export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base text for subtitle of Flow Settings (Menubar)
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base text for subtitle of Code Dialog (Toolbar)
|
* The base text for subtitle of Code Dialog (Toolbar)
|
||||||
|
|
@ -18,22 +27,382 @@ export const CODE_DIALOG_SUBTITLE =
|
||||||
* @constant
|
* @constant
|
||||||
*/
|
*/
|
||||||
export const EDIT_DIALOG_SUBTITLE =
|
export const EDIT_DIALOG_SUBTITLE =
|
||||||
"Make configurations changes to your nodes. Click save when you're done.";
|
"Adjust the configurations of your component. Define parameter visibility for the canvas view. Remember to save once you’re finished.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base text for subtitle of Code Dialog
|
* The base text for subtitle of Code Dialog
|
||||||
* @constant
|
* @constant
|
||||||
*/
|
*/
|
||||||
export const CODE_PROMPT_DIALOG_SUBTITLE = "Edit you python code.";
|
export const CODE_PROMPT_DIALOG_SUBTITLE =
|
||||||
|
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base text for subtitle of Prompt Dialog
|
* The base text for subtitle of Prompt Dialog
|
||||||
* @constant
|
* @constant
|
||||||
*/
|
*/
|
||||||
export const PROMPT_DIALOG_SUBTITLE = "Edit you prompt.";
|
export const PROMPT_DIALOG_SUBTITLE =
|
||||||
|
"Create your prompt. Prompts can help guide the behavior of a Language Model.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base text for subtitle of Text Dialog
|
* The base text for subtitle of Text Dialog
|
||||||
* @constant
|
* @constant
|
||||||
*/
|
*/
|
||||||
export const TEXT_DIALOG_SUBTITLE = "Edit you text.";
|
export const TEXT_DIALOG_SUBTITLE = "Edit your text.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get the python code for the API
|
||||||
|
* @param {string} flowId - The id of the flow
|
||||||
|
* @returns {string} - The python code
|
||||||
|
*/
|
||||||
|
export const getPythonApiCode = (flow: FlowType): string => {
|
||||||
|
const flowId = flow.id;
|
||||||
|
|
||||||
|
// create a dictionary of node ids and the values is an empty dictionary
|
||||||
|
// flow.data.nodes.forEach((node) => {
|
||||||
|
// node.data.id
|
||||||
|
// }
|
||||||
|
const tweaks = buildTweaks(flow);
|
||||||
|
return `import requests
|
||||||
|
|
||||||
|
BASE_API_URL = "${window.location.protocol}//${
|
||||||
|
window.location.host
|
||||||
|
}/api/v1/predict"
|
||||||
|
FLOW_ID = "${flowId}"
|
||||||
|
# You can tweak the flow by adding a tweaks dictionary
|
||||||
|
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
|
||||||
|
TWEAKS = ${JSON.stringify(tweaks, null, 2)}
|
||||||
|
|
||||||
|
def run_flow(message: str, flow_id: str, tweaks: dict = None) -> dict:
|
||||||
|
"""
|
||||||
|
Run a flow with a given message and optional tweaks.
|
||||||
|
|
||||||
|
:param message: The message to send to the flow
|
||||||
|
:param flow_id: The ID of the flow to run
|
||||||
|
:param tweaks: Optional tweaks to customize the flow
|
||||||
|
:return: The JSON response from the flow
|
||||||
|
"""
|
||||||
|
api_url = f"{BASE_API_URL}/{flow_id}"
|
||||||
|
|
||||||
|
payload = {"message": message}
|
||||||
|
|
||||||
|
if tweaks:
|
||||||
|
payload["tweaks"] = tweaks
|
||||||
|
|
||||||
|
response = requests.post(api_url, json=payload)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Setup any tweaks you want to apply to the flow
|
||||||
|
|
||||||
|
print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))`;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Function to get the curl code for the API
|
||||||
|
* @param {string} flowId - The id of the flow
|
||||||
|
* @returns {string} - The curl code
|
||||||
|
*/
|
||||||
|
export const getCurlCode = (flow: FlowType): string => {
|
||||||
|
const flowId = flow.id;
|
||||||
|
const tweaks = buildTweaks(flow);
|
||||||
|
return `curl -X POST \\
|
||||||
|
${window.location.protocol}//${
|
||||||
|
window.location.host
|
||||||
|
}/api/v1/predict/${flowId} \\
|
||||||
|
-H 'Content-Type: application/json' \\
|
||||||
|
-d '{"message": "Your message", "tweaks": ${JSON.stringify(
|
||||||
|
tweaks,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}}'`;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Function to get the python code for the API
|
||||||
|
* @param {string} flowName - The name of the flow
|
||||||
|
* @returns {string} - The python code
|
||||||
|
*/
|
||||||
|
export const getPythonCode = (flow: FlowType): string => {
|
||||||
|
const flowName = flow.name;
|
||||||
|
return `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?")`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base text for subtitle of Import Dialog
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const IMPORT_DIALOG_SUBTITLE =
|
||||||
|
"Upload a JSON file or select from the available community examples.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base text for subtitle of code dialog
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const EXPORT_CODE_DIALOG =
|
||||||
|
"Generate the code to integrate your flow into an external application.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base text for subtitle of code dialog
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const INPUT_STYLE =
|
||||||
|
" focus:ring-1 focus:ring-offset-1 focus:ring-ring focus:outline-none ";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default description for the flow
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DESCRIPTIONS: string[] = [
|
||||||
|
"Chain the Words, Master Language!",
|
||||||
|
"Language Architect at Work!",
|
||||||
|
"Empowering Language Engineering.",
|
||||||
|
"Craft Language Connections Here.",
|
||||||
|
"Create, Connect, Converse.",
|
||||||
|
"Smart Chains, Smarter Conversations.",
|
||||||
|
"Bridging Prompts for Brilliance.",
|
||||||
|
"Language Models, Unleashed.",
|
||||||
|
"Your Hub for Text Generation.",
|
||||||
|
"Promptly Ingenious!",
|
||||||
|
"Building Linguistic Labyrinths.",
|
||||||
|
"LangFlow: Create, Chain, Communicate.",
|
||||||
|
"Connect the Dots, Craft Language.",
|
||||||
|
"Interactive Language Weaving.",
|
||||||
|
"Generate, Innovate, Communicate.",
|
||||||
|
"Conversation Catalyst Engine.",
|
||||||
|
"Language Chainlink Master.",
|
||||||
|
"Design Dialogues with LangFlow.",
|
||||||
|
"Nurture NLP Nodes Here.",
|
||||||
|
"Conversational Cartography Unlocked.",
|
||||||
|
"Design, Develop, Dialogize.",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjectives for the name of the flow
|
||||||
|
* @constant
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const ADJECTIVES: string[] = [
|
||||||
|
"admiring",
|
||||||
|
"adoring",
|
||||||
|
"agitated",
|
||||||
|
"amazing",
|
||||||
|
"angry",
|
||||||
|
"awesome",
|
||||||
|
"backstabbing",
|
||||||
|
"berserk",
|
||||||
|
"big",
|
||||||
|
"boring",
|
||||||
|
"clever",
|
||||||
|
"cocky",
|
||||||
|
"compassionate",
|
||||||
|
"condescending",
|
||||||
|
"cranky",
|
||||||
|
"desperate",
|
||||||
|
"determined",
|
||||||
|
"distracted",
|
||||||
|
"dreamy",
|
||||||
|
"drunk",
|
||||||
|
"ecstatic",
|
||||||
|
"elated",
|
||||||
|
"elegant",
|
||||||
|
"evil",
|
||||||
|
"fervent",
|
||||||
|
"focused",
|
||||||
|
"furious",
|
||||||
|
"gigantic",
|
||||||
|
"gloomy",
|
||||||
|
"goofy",
|
||||||
|
"grave",
|
||||||
|
"happy",
|
||||||
|
"high",
|
||||||
|
"hopeful",
|
||||||
|
"hungry",
|
||||||
|
"insane",
|
||||||
|
"jolly",
|
||||||
|
"jovial",
|
||||||
|
"kickass",
|
||||||
|
"lonely",
|
||||||
|
"loving",
|
||||||
|
"mad",
|
||||||
|
"modest",
|
||||||
|
"naughty",
|
||||||
|
"nauseous",
|
||||||
|
"nostalgic",
|
||||||
|
"pedantic",
|
||||||
|
"pensive",
|
||||||
|
"prickly",
|
||||||
|
"reverent",
|
||||||
|
"romantic",
|
||||||
|
"sad",
|
||||||
|
"serene",
|
||||||
|
"sharp",
|
||||||
|
"sick",
|
||||||
|
"silly",
|
||||||
|
"sleepy",
|
||||||
|
"small",
|
||||||
|
"stoic",
|
||||||
|
"stupefied",
|
||||||
|
"suspicious",
|
||||||
|
"tender",
|
||||||
|
"thirsty",
|
||||||
|
"tiny",
|
||||||
|
"trusting",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nouns for the name of the flow
|
||||||
|
* @constant
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const NOUNS: string[] = [
|
||||||
|
"albattani",
|
||||||
|
"allen",
|
||||||
|
"almeida",
|
||||||
|
"archimedes",
|
||||||
|
"ardinghelli",
|
||||||
|
"aryabhata",
|
||||||
|
"austin",
|
||||||
|
"babbage",
|
||||||
|
"banach",
|
||||||
|
"bardeen",
|
||||||
|
"bartik",
|
||||||
|
"bassi",
|
||||||
|
"bell",
|
||||||
|
"bhabha",
|
||||||
|
"bhaskara",
|
||||||
|
"blackwell",
|
||||||
|
"bohr",
|
||||||
|
"booth",
|
||||||
|
"borg",
|
||||||
|
"bose",
|
||||||
|
"boyd",
|
||||||
|
"brahmagupta",
|
||||||
|
"brattain",
|
||||||
|
"brown",
|
||||||
|
"carson",
|
||||||
|
"chandrasekhar",
|
||||||
|
"colden",
|
||||||
|
"cori",
|
||||||
|
"cray",
|
||||||
|
"curie",
|
||||||
|
"darwin",
|
||||||
|
"davinci",
|
||||||
|
"dijkstra",
|
||||||
|
"dubinsky",
|
||||||
|
"easley",
|
||||||
|
"einstein",
|
||||||
|
"elion",
|
||||||
|
"engelbart",
|
||||||
|
"euclid",
|
||||||
|
"euler",
|
||||||
|
"fermat",
|
||||||
|
"fermi",
|
||||||
|
"feynman",
|
||||||
|
"franklin",
|
||||||
|
"galileo",
|
||||||
|
"gates",
|
||||||
|
"goldberg",
|
||||||
|
"goldstine",
|
||||||
|
"goldwasser",
|
||||||
|
"golick",
|
||||||
|
"goodall",
|
||||||
|
"hamilton",
|
||||||
|
"hawking",
|
||||||
|
"heisenberg",
|
||||||
|
"heyrovsky",
|
||||||
|
"hodgkin",
|
||||||
|
"hoover",
|
||||||
|
"hopper",
|
||||||
|
"hugle",
|
||||||
|
"hypatia",
|
||||||
|
"jang",
|
||||||
|
"jennings",
|
||||||
|
"jepsen",
|
||||||
|
"joliot",
|
||||||
|
"jones",
|
||||||
|
"kalam",
|
||||||
|
"kare",
|
||||||
|
"keller",
|
||||||
|
"khorana",
|
||||||
|
"kilby",
|
||||||
|
"kirch",
|
||||||
|
"knuth",
|
||||||
|
"kowalevski",
|
||||||
|
"lalande",
|
||||||
|
"lamarr",
|
||||||
|
"leakey",
|
||||||
|
"leavitt",
|
||||||
|
"lichterman",
|
||||||
|
"liskov",
|
||||||
|
"lovelace",
|
||||||
|
"lumiere",
|
||||||
|
"mahavira",
|
||||||
|
"mayer",
|
||||||
|
"mccarthy",
|
||||||
|
"mcclintock",
|
||||||
|
"mclean",
|
||||||
|
"mcnulty",
|
||||||
|
"meitner",
|
||||||
|
"meninsky",
|
||||||
|
"mestorf",
|
||||||
|
"minsky",
|
||||||
|
"mirzakhani",
|
||||||
|
"morse",
|
||||||
|
"murdock",
|
||||||
|
"newton",
|
||||||
|
"nobel",
|
||||||
|
"noether",
|
||||||
|
"northcutt",
|
||||||
|
"noyce",
|
||||||
|
"panini",
|
||||||
|
"pare",
|
||||||
|
"pasteur",
|
||||||
|
"payne",
|
||||||
|
"perlman",
|
||||||
|
"pike",
|
||||||
|
"poincare",
|
||||||
|
"poitras",
|
||||||
|
"ptolemy",
|
||||||
|
"raman",
|
||||||
|
"ramanujan",
|
||||||
|
"ride",
|
||||||
|
"ritchie",
|
||||||
|
"roentgen",
|
||||||
|
"rosalind",
|
||||||
|
"saha",
|
||||||
|
"sammet",
|
||||||
|
"shaw",
|
||||||
|
"shirley",
|
||||||
|
"shockley",
|
||||||
|
"sinoussi",
|
||||||
|
"snyder",
|
||||||
|
"spence",
|
||||||
|
"stallman",
|
||||||
|
"stonebraker",
|
||||||
|
"swanson",
|
||||||
|
"swartz",
|
||||||
|
"swirles",
|
||||||
|
"tesla",
|
||||||
|
"thompson",
|
||||||
|
"torvalds",
|
||||||
|
"turing",
|
||||||
|
"varahamihira",
|
||||||
|
"visvesvaraya",
|
||||||
|
"volhard",
|
||||||
|
"wescoff",
|
||||||
|
"williams",
|
||||||
|
"wilson",
|
||||||
|
"wing",
|
||||||
|
"wozniak",
|
||||||
|
"wright",
|
||||||
|
"yalow",
|
||||||
|
"yonath",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header text for user projects
|
||||||
|
* @constant
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const USER_PROJECTS_HEADER = "My Collection";
|
||||||
|
|
|
||||||
40
src/frontend/src/contexts/SSEContext.tsx
Normal file
40
src/frontend/src/contexts/SSEContext.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const initialValue = {
|
||||||
|
updateSSEData: ({}) => {},
|
||||||
|
sseData: {},
|
||||||
|
isBuilding: false,
|
||||||
|
setIsBuilding: (isBuilding: boolean) => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SSEContext = createContext(initialValue);
|
||||||
|
|
||||||
|
export function useSSE() {
|
||||||
|
return useContext(SSEContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SSEProvider({ children }) {
|
||||||
|
const [sseData, setSSEData] = useState({});
|
||||||
|
const [isBuilding, setIsBuilding] = useState(false);
|
||||||
|
|
||||||
|
const updateSSEData = useCallback((newData: any) => {
|
||||||
|
setSSEData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
...newData,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSEContext.Provider
|
||||||
|
value={{ sseData, updateSSEData, isBuilding, setIsBuilding }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SSEContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
||||||
function setErrorData(newState: { title: string; list?: Array<string> }) {
|
function setErrorData(newState: { title: string; list?: Array<string> }) {
|
||||||
setErrorDataState(newState);
|
setErrorDataState(newState);
|
||||||
setErrorOpen(true);
|
setErrorOpen(true);
|
||||||
if (newState.title) {
|
if (newState.title && newState.title !== "") {
|
||||||
setNotificationCenter(true);
|
setNotificationCenter(true);
|
||||||
pushNotificationList({
|
pushNotificationList({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|
@ -97,7 +97,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
||||||
function setNoticeData(newState: { title: string; link?: string }) {
|
function setNoticeData(newState: { title: string; link?: string }) {
|
||||||
setNoticeDataState(newState);
|
setNoticeDataState(newState);
|
||||||
setNoticeOpen(true);
|
setNoticeOpen(true);
|
||||||
if (newState.title) {
|
if (newState.title && newState.title !== "") {
|
||||||
// Add new notice to notification center
|
// Add new notice to notification center
|
||||||
setNotificationCenter(true);
|
setNotificationCenter(true);
|
||||||
pushNotificationList({
|
pushNotificationList({
|
||||||
|
|
@ -117,7 +117,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
||||||
setSuccessOpen(true); // open the success alert
|
setSuccessOpen(true); // open the success alert
|
||||||
|
|
||||||
// If the new state has a "title" property, add a new success notification to the list
|
// If the new state has a "title" property, add a new success notification to the list
|
||||||
if (newState.title) {
|
if (newState.title && newState.title !== "") {
|
||||||
setNotificationCenter(true); // show the notification center
|
setNotificationCenter(true); // show the notification center
|
||||||
pushNotificationList({
|
pushNotificationList({
|
||||||
// add the new notification to the list
|
// add the new notification to the list
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,31 @@ import { LocationProvider } from "./locationContext";
|
||||||
import PopUpProvider from "./popUpContext";
|
import PopUpProvider from "./popUpContext";
|
||||||
import { TabsProvider } from "./tabsContext";
|
import { TabsProvider } from "./tabsContext";
|
||||||
import { TypesProvider } from "./typesContext";
|
import { TypesProvider } from "./typesContext";
|
||||||
|
import { ReactFlowProvider } from "reactflow";
|
||||||
|
import { UndoRedoProvider } from "./undoRedoContext";
|
||||||
|
import { SSEProvider } from "./SSEContext";
|
||||||
|
|
||||||
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
||||||
//element to wrap all context
|
//element to wrap all context
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ReactFlowProvider>
|
||||||
<DarkProvider>
|
<DarkProvider>
|
||||||
<TypesProvider>
|
<TypesProvider>
|
||||||
<LocationProvider>
|
<LocationProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
|
<SSEProvider>
|
||||||
<TabsProvider>
|
<TabsProvider>
|
||||||
|
<UndoRedoProvider>
|
||||||
<PopUpProvider>{children}</PopUpProvider>
|
<PopUpProvider>{children}</PopUpProvider>
|
||||||
|
</UndoRedoProvider>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
|
</SSEProvider>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</LocationProvider>
|
</LocationProvider>
|
||||||
</TypesProvider>
|
</TypesProvider>
|
||||||
</DarkProvider>
|
</DarkProvider>
|
||||||
|
</ReactFlowProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,38 +7,52 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { FlowType, NodeType } from "../types/flow";
|
import { FlowType, NodeType } from "../types/flow";
|
||||||
import { LangFlowState, TabsContextType } from "../types/tabs";
|
import { TabsContextType, TabsState } from "../types/tabs";
|
||||||
import {
|
import {
|
||||||
normalCaseToSnakeCase,
|
|
||||||
updateIds,
|
updateIds,
|
||||||
updateObject,
|
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
|
getRandomDescription,
|
||||||
|
getRandomName,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { alertContext } from "./alertContext";
|
import { alertContext } from "./alertContext";
|
||||||
import { typesContext } from "./typesContext";
|
import { typesContext } from "./typesContext";
|
||||||
import { APITemplateType, TemplateVariableType } from "../types/api";
|
import { APITemplateType } from "../types/api";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { addEdge } from "reactflow";
|
import { addEdge } from "reactflow";
|
||||||
|
import {
|
||||||
|
readFlowsFromDatabase,
|
||||||
|
deleteFlowFromDatabase,
|
||||||
|
saveFlowToDatabase,
|
||||||
|
downloadFlowsFromDatabase,
|
||||||
|
uploadFlowsToDatabase,
|
||||||
|
updateFlowInDatabase,
|
||||||
|
} from "../controllers/API";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const uid = new ShortUniqueId({ length: 5 });
|
||||||
|
|
||||||
const TabsContextInitialValue: TabsContextType = {
|
const TabsContextInitialValue: TabsContextType = {
|
||||||
save: () => {},
|
save: () => {},
|
||||||
tabIndex: 0,
|
tabId: "",
|
||||||
setTabIndex: (index: number) => {},
|
setTabId: (index: string) => {},
|
||||||
flows: [],
|
flows: [],
|
||||||
removeFlow: (id: string) => {},
|
removeFlow: (id: string) => {},
|
||||||
addFlow: (flowData?: any) => {},
|
addFlow: async (flowData?: any) => "",
|
||||||
updateFlow: (newFlow: FlowType) => {},
|
updateFlow: (newFlow: FlowType) => {},
|
||||||
incrementNodeId: () => uuidv4(),
|
incrementNodeId: () => uid(),
|
||||||
downloadFlow: (flow: FlowType) => {},
|
downloadFlow: (flow: FlowType) => {},
|
||||||
|
downloadFlows: () => {},
|
||||||
|
uploadFlows: () => {},
|
||||||
uploadFlow: () => {},
|
uploadFlow: () => {},
|
||||||
hardReset: () => {},
|
hardReset: () => {},
|
||||||
|
saveFlow: async (flow: FlowType) => {},
|
||||||
disableCopyPaste: false,
|
disableCopyPaste: false,
|
||||||
setDisableCopyPaste: (state: boolean) => {},
|
setDisableCopyPaste: (state: boolean) => {},
|
||||||
lastCopiedSelection: null,
|
lastCopiedSelection: null,
|
||||||
setLastCopiedSelection: (selection: any) => {},
|
setLastCopiedSelection: (selection: any) => {},
|
||||||
|
tabsState: {},
|
||||||
getNodeId: () => "",
|
setTabsState: (state: TabsState) => {},
|
||||||
|
getNodeId: (nodeType: string) => "",
|
||||||
paste: (
|
paste: (
|
||||||
selection: { nodes: any; edges: any },
|
selection: { nodes: any; edges: any },
|
||||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||||
|
|
@ -51,17 +65,21 @@ export const TabsContext = createContext<TabsContextType>(
|
||||||
|
|
||||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
const { setErrorData, setNoticeData } = useContext(alertContext);
|
const { setErrorData, setNoticeData } = useContext(alertContext);
|
||||||
const [tabIndex, setTabIndex] = useState(0);
|
|
||||||
|
const [tabId, setTabId] = useState("");
|
||||||
|
|
||||||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||||
const [id, setId] = useState(uuidv4());
|
const [id, setId] = useState(uid());
|
||||||
const { templates, reactFlowInstance } = useContext(typesContext);
|
const { templates, reactFlowInstance } = useContext(typesContext);
|
||||||
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
|
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
|
||||||
|
const [tabsState, setTabsState] = useState<TabsState>({});
|
||||||
|
|
||||||
const newNodeId = useRef(uuidv4());
|
const newNodeId = useRef(uid());
|
||||||
function incrementNodeId() {
|
function incrementNodeId() {
|
||||||
newNodeId.current = uuidv4();
|
newNodeId.current = uid();
|
||||||
return newNodeId.current;
|
return newNodeId.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
// added clone deep to avoid mutating the original object
|
// added clone deep to avoid mutating the original object
|
||||||
let Saveflows = _.cloneDeep(flows);
|
let Saveflows = _.cloneDeep(flows);
|
||||||
|
|
@ -83,26 +101,104 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
"tabsData",
|
"tabsData",
|
||||||
JSON.stringify({ tabIndex, flows: Saveflows, id })
|
JSON.stringify({ tabId, flows: Saveflows, id })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// function loadCookie(cookie: string) {
|
||||||
//get tabs locally saved
|
// if (cookie && Object.keys(templates).length > 0) {
|
||||||
let cookie = window.localStorage.getItem("tabsData");
|
// let cookieObject: LangFlowState = JSON.parse(cookie);
|
||||||
if (cookie && Object.keys(templates).length > 0) {
|
// try {
|
||||||
let cookieObject: LangFlowState = JSON.parse(cookie);
|
// cookieObject.flows.forEach((flow) => {
|
||||||
|
// if (!flow.data) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// flow.data.edges.forEach((edge) => {
|
||||||
|
// edge.className = "";
|
||||||
|
// edge.style = { stroke: "#555555" };
|
||||||
|
// });
|
||||||
|
|
||||||
|
// flow.data.nodes.forEach((node) => {
|
||||||
|
// const template = templates[node.data.type];
|
||||||
|
// if (!template) {
|
||||||
|
// setErrorData({ title: `Unknown node type: ${node.data.type}` });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (Object.keys(template["template"]).length > 0) {
|
||||||
|
// node.data.node.base_classes = template["base_classes"];
|
||||||
|
// flow.data.edges.forEach((edge) => {
|
||||||
|
// if (edge.source === node.id) {
|
||||||
|
// edge.sourceHandle = edge.sourceHandle
|
||||||
|
// .split("|")
|
||||||
|
// .slice(0, 2)
|
||||||
|
// .concat(template["base_classes"])
|
||||||
|
// .join("|");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// node.data.node.description = template["description"];
|
||||||
|
// node.data.node.template = updateTemplate(
|
||||||
|
// template["template"] as unknown as APITemplateType,
|
||||||
|
// node.data.node.template as APITemplateType
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// setTabIndex(cookieObject.tabIndex);
|
||||||
|
// setFlows(cookieObject.flows);
|
||||||
|
// setId(cookieObject.id);
|
||||||
|
// } catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
function refreshFlows() {
|
||||||
|
getTabsDataFromDB().then((DbData) => {
|
||||||
|
if (DbData && Object.keys(templates).length > 0) {
|
||||||
|
try {
|
||||||
|
processDBData(DbData);
|
||||||
|
updateStateWithDbData(DbData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// get data from db
|
||||||
|
//get tabs locally saved
|
||||||
|
// let tabsData = getLocalStorageTabsData();
|
||||||
|
refreshFlows();
|
||||||
|
}, [templates]);
|
||||||
|
|
||||||
|
function getTabsDataFromDB() {
|
||||||
|
//get tabs from db
|
||||||
|
return readFlowsFromDatabase();
|
||||||
|
}
|
||||||
|
function processDBData(DbData) {
|
||||||
|
DbData.forEach((flow) => {
|
||||||
try {
|
try {
|
||||||
cookieObject.flows.forEach((flow) => {
|
|
||||||
if (!flow.data) {
|
if (!flow.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
processFlowEdges(flow);
|
||||||
|
processFlowNodes(flow);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFlowEdges(flow) {
|
||||||
flow.data.edges.forEach((edge) => {
|
flow.data.edges.forEach((edge) => {
|
||||||
edge.className = "";
|
edge.className = "";
|
||||||
edge.style = { stroke: "#555555" };
|
edge.style = { stroke: "#555555" };
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFlowNodes(flow) {
|
||||||
flow.data.nodes.forEach((node) => {
|
flow.data.nodes.forEach((node) => {
|
||||||
const template = templates[node.data.type];
|
const template = templates[node.data.type];
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|
@ -110,7 +206,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Object.keys(template["template"]).length > 0) {
|
if (Object.keys(template["template"]).length > 0) {
|
||||||
|
updateNodeBaseClasses(node, template);
|
||||||
|
updateNodeEdges(flow, node, template);
|
||||||
|
updateNodeDescription(node, template);
|
||||||
|
updateNodeTemplate(node, template);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeBaseClasses(node, template) {
|
||||||
node.data.node.base_classes = template["base_classes"];
|
node.data.node.base_classes = template["base_classes"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeEdges(flow, node, template) {
|
||||||
flow.data.edges.forEach((edge) => {
|
flow.data.edges.forEach((edge) => {
|
||||||
if (edge.source === node.id) {
|
if (edge.source === node.id) {
|
||||||
edge.sourceHandle = edge.sourceHandle
|
edge.sourceHandle = edge.sourceHandle
|
||||||
|
|
@ -120,34 +228,29 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
.join("|");
|
.join("|");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeDescription(node, template) {
|
||||||
node.data.node.description = template["description"];
|
node.data.node.description = template["description"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeTemplate(node, template) {
|
||||||
node.data.node.template = updateTemplate(
|
node.data.node.template = updateTemplate(
|
||||||
template["template"] as unknown as APITemplateType,
|
template["template"] as unknown as APITemplateType,
|
||||||
node.data.node.template as APITemplateType
|
node.data.node.template as APITemplateType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
setTabIndex(cookieObject.tabIndex);
|
|
||||||
setFlows(cookieObject.flows);
|
|
||||||
setId(cookieObject.id);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [templates]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
function updateStateWithDbData(tabsData) {
|
||||||
//save tabs locally
|
setFlows(tabsData);
|
||||||
// console.log(id);
|
}
|
||||||
save();
|
|
||||||
}, [flows, id, tabIndex, newNodeId]);
|
|
||||||
|
|
||||||
function hardReset() {
|
function hardReset() {
|
||||||
newNodeId.current = uuidv4();
|
newNodeId.current = uid();
|
||||||
setTabIndex(0);
|
setTabId("");
|
||||||
|
|
||||||
setFlows([]);
|
setFlows([]);
|
||||||
setId(uuidv4());
|
setId(uid());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -162,7 +265,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
// create a link element and set its properties
|
// create a link element and set its properties
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = jsonString;
|
link.href = jsonString;
|
||||||
link.download = `${flows[tabIndex].name}.json`;
|
link.download = `${flows.find((f) => f.id === tabId).name}.json`;
|
||||||
|
|
||||||
// simulate a click on the link element to trigger the download
|
// simulate a click on the link element to trigger the download
|
||||||
link.click();
|
link.click();
|
||||||
|
|
@ -171,8 +274,24 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeId() {
|
function downloadFlows() {
|
||||||
return `dndnode_` + incrementNodeId();
|
downloadFlowsFromDatabase().then((flows) => {
|
||||||
|
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
|
||||||
|
JSON.stringify(flows)
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
// create a link element and set its properties
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = jsonString;
|
||||||
|
link.download = `flows.json`;
|
||||||
|
|
||||||
|
// simulate a click on the link element to trigger the download
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeId(nodeType: string) {
|
||||||
|
return nodeType + "-" + incrementNodeId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -180,10 +299,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
* If the file type is application/json, the file is read and parsed into a JSON object.
|
* If the file type is application/json, the file is read and parsed into a JSON object.
|
||||||
* The resulting JSON object is passed to the addFlow function.
|
* The resulting JSON object is passed to the addFlow function.
|
||||||
*/
|
*/
|
||||||
function uploadFlow() {
|
function uploadFlow(newProject?: boolean) {
|
||||||
// create a file input
|
// create a file input
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
|
input.accept = ".json";
|
||||||
// add a change event listener to the file input
|
// add a change event listener to the file input
|
||||||
input.onchange = (e: Event) => {
|
input.onchange = (e: Event) => {
|
||||||
// check if the file type is application/json
|
// check if the file type is application/json
|
||||||
|
|
@ -195,7 +315,29 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
// parse the text into a JSON object
|
// parse the text into a JSON object
|
||||||
let flow: FlowType = JSON.parse(text);
|
let flow: FlowType = JSON.parse(text);
|
||||||
|
|
||||||
addFlow(flow);
|
addFlow(flow, newProject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// trigger the file input click event to open the file dialog
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFlows() {
|
||||||
|
// create a file input
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
// add a change event listener to the file input
|
||||||
|
input.onchange = (e: Event) => {
|
||||||
|
// check if the file type is application/json
|
||||||
|
if ((e.target as HTMLInputElement).files[0].type === "application/json") {
|
||||||
|
// get the file from the file input
|
||||||
|
const file = (e.target as HTMLInputElement).files[0];
|
||||||
|
// read the file as text
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
uploadFlowsToDatabase(formData).then(() => {
|
||||||
|
refreshFlows();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -208,22 +350,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
* @param {string} id - The id of the flow to remove.
|
* @param {string} id - The id of the flow to remove.
|
||||||
*/
|
*/
|
||||||
function removeFlow(id: string) {
|
function removeFlow(id: string) {
|
||||||
setFlows((prevState) => {
|
const index = flows.findIndex((flow) => flow.id === id);
|
||||||
const newFlows = [...prevState];
|
|
||||||
const index = newFlows.findIndex((flow) => flow.id === id);
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
if (index === tabIndex) {
|
deleteFlowFromDatabase(id).then(() => {
|
||||||
setTabIndex(flows.length - 2);
|
setFlows(flows.filter((flow) => flow.id !== id));
|
||||||
newFlows.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
let flowId = flows[tabIndex].id;
|
|
||||||
newFlows.splice(index, 1);
|
|
||||||
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newFlows;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Add a new flow to the list of flows.
|
* Add a new flow to the list of flows.
|
||||||
* @param flow Optional flow to add.
|
* @param flow Optional flow to add.
|
||||||
|
|
@ -251,9 +384,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
? { x: position.paneX + position.x, y: position.paneY + position.y }
|
? { x: position.paneX + position.x, y: position.paneY + position.y }
|
||||||
: reactFlowInstance.project({ x: position.x, y: position.y });
|
: reactFlowInstance.project({ x: position.x, y: position.y });
|
||||||
|
|
||||||
selectionInstance.nodes.forEach((n) => {
|
selectionInstance.nodes.forEach((n: NodeType) => {
|
||||||
// Generate a unique node ID
|
// Generate a unique node ID
|
||||||
let newId = getNodeId();
|
let newId = getNodeId(n.data.type);
|
||||||
idsMap[n.id] = newId;
|
idsMap[n.id] = newId;
|
||||||
|
|
||||||
// Create a new node object
|
// Create a new node object
|
||||||
|
|
@ -318,13 +451,57 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
reactFlowInstance.setEdges(edges);
|
reactFlowInstance.setEdges(edges);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addFlow(flow?: FlowType) {
|
const addFlow = async (
|
||||||
// Get data from the flow or set it to null if there's no flow provided.
|
flow?: FlowType,
|
||||||
|
newProject?: Boolean
|
||||||
|
): Promise<String> => {
|
||||||
|
if (newProject) {
|
||||||
|
let flowData = extractDataFromFlow(flow);
|
||||||
|
if (flowData.description == "") {
|
||||||
|
flowData.description = getRandomDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new flow with a default name if no flow is provided.
|
||||||
|
const newFlow = createNewFlow(flowData, flow);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = await saveFlowToDatabase(newFlow);
|
||||||
|
// Change the id to the new id.
|
||||||
|
newFlow.id = id;
|
||||||
|
|
||||||
|
// Add the new flow to the list of flows.
|
||||||
|
addFlowToLocalState(newFlow);
|
||||||
|
|
||||||
|
// Return the id
|
||||||
|
return id;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle the error if needed
|
||||||
|
console.error("Error while adding flow:", error);
|
||||||
|
throw error; // Re-throw the error so the caller can handle it if needed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paste(
|
||||||
|
{ nodes: flow.data.nodes, edges: flow.data.edges },
|
||||||
|
{ x: 10, y: 10 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractDataFromFlow = (flow) => {
|
||||||
let data = flow?.data ? flow.data : null;
|
let data = flow?.data ? flow.data : null;
|
||||||
const description = flow?.description ? flow.description : "";
|
const description = flow?.description ? flow.description : "";
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
data.edges.forEach((edge) => {
|
updateEdges(data.edges);
|
||||||
|
updateNodes(data.nodes, data.edges);
|
||||||
|
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, description };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEdges = (edges) => {
|
||||||
|
edges.forEach((edge) => {
|
||||||
edge.style = { stroke: "inherit" };
|
edge.style = { stroke: "inherit" };
|
||||||
edge.className =
|
edge.className =
|
||||||
edge.targetHandle.split("|")[0] === "Text"
|
edge.targetHandle.split("|")[0] === "Text"
|
||||||
|
|
@ -332,7 +509,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
: "stroke-gray-900 dark:stroke-gray-200";
|
: "stroke-gray-900 dark:stroke-gray-200";
|
||||||
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
||||||
});
|
});
|
||||||
data.nodes.forEach((node) => {
|
};
|
||||||
|
|
||||||
|
const updateNodes = (nodes, edges) => {
|
||||||
|
nodes.forEach((node) => {
|
||||||
const template = templates[node.data.type];
|
const template = templates[node.data.type];
|
||||||
if (!template) {
|
if (!template) {
|
||||||
setErrorData({ title: `Unknown node type: ${node.data.type}` });
|
setErrorData({ title: `Unknown node type: ${node.data.type}` });
|
||||||
|
|
@ -340,7 +520,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
if (Object.keys(template["template"]).length > 0) {
|
if (Object.keys(template["template"]).length > 0) {
|
||||||
node.data.node.base_classes = template["base_classes"];
|
node.data.node.base_classes = template["base_classes"];
|
||||||
flow.data.edges.forEach((edge) => {
|
edges.forEach((edge) => {
|
||||||
if (edge.source === node.id) {
|
if (edge.source === node.id) {
|
||||||
edge.sourceHandle = edge.sourceHandle
|
edge.sourceHandle = edge.sourceHandle
|
||||||
.split("|")
|
.split("|")
|
||||||
|
|
@ -356,29 +536,21 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updateIds(data, getNodeId);
|
|
||||||
}
|
|
||||||
// Create a new flow with a default name if no flow is provided.
|
|
||||||
let newFlow: FlowType = {
|
|
||||||
description,
|
|
||||||
name: flow?.name ?? "New Flow",
|
|
||||||
id: uuidv4(),
|
|
||||||
data,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Increment the ID counter.
|
const createNewFlow = (flowData, flow) => ({
|
||||||
setId(uuidv4());
|
description: flowData.description,
|
||||||
|
name: flow?.name ?? getRandomName(),
|
||||||
// Add the new flow to the list of flows.
|
data: flowData.data,
|
||||||
|
id: "",
|
||||||
setFlows((prevState) => {
|
|
||||||
const newFlows = [...prevState, newFlow];
|
|
||||||
return newFlows;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the tab index to the new flow.
|
const addFlowToLocalState = (newFlow) => {
|
||||||
setTabIndex(flows.length);
|
setFlows((prevState) => {
|
||||||
}
|
return [...prevState, newFlow];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing flow with new data
|
* Updates an existing flow with new data
|
||||||
* @param newFlow - The new flow object containing the updated data
|
* @param newFlow - The new flow object containing the updated data
|
||||||
|
|
@ -395,27 +567,64 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
return newFlows;
|
return newFlows;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveFlow(newFlow: FlowType) {
|
||||||
|
try {
|
||||||
|
// updates flow in db
|
||||||
|
const updatedFlow = await updateFlowInDatabase(newFlow);
|
||||||
|
if (updatedFlow) {
|
||||||
|
// updates flow in state
|
||||||
|
setFlows((prevState) => {
|
||||||
|
const newFlows = [...prevState];
|
||||||
|
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
newFlows[index].description = newFlow.description ?? "";
|
||||||
|
newFlows[index].data = newFlow.data;
|
||||||
|
newFlows[index].name = newFlow.name;
|
||||||
|
}
|
||||||
|
return newFlows;
|
||||||
|
});
|
||||||
|
//update tabs state
|
||||||
|
setTabsState((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[tabId]: {
|
||||||
|
isPending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setErrorData(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [disableCopyPaste, setDisableCopyPaste] = useState(false);
|
const [disableCopyPaste, setDisableCopyPaste] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContext.Provider
|
<TabsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
saveFlow,
|
||||||
lastCopiedSelection,
|
lastCopiedSelection,
|
||||||
setLastCopiedSelection,
|
setLastCopiedSelection,
|
||||||
disableCopyPaste,
|
disableCopyPaste,
|
||||||
setDisableCopyPaste,
|
setDisableCopyPaste,
|
||||||
save,
|
|
||||||
hardReset,
|
hardReset,
|
||||||
tabIndex,
|
tabId,
|
||||||
setTabIndex,
|
setTabId,
|
||||||
flows,
|
flows,
|
||||||
|
save,
|
||||||
incrementNodeId,
|
incrementNodeId,
|
||||||
removeFlow,
|
removeFlow,
|
||||||
addFlow,
|
addFlow,
|
||||||
updateFlow,
|
updateFlow,
|
||||||
downloadFlow,
|
downloadFlow,
|
||||||
|
downloadFlows,
|
||||||
|
uploadFlows,
|
||||||
uploadFlow,
|
uploadFlow,
|
||||||
getNodeId,
|
getNodeId,
|
||||||
|
tabsState,
|
||||||
|
setTabsState,
|
||||||
paste,
|
paste,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,19 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
||||||
);
|
);
|
||||||
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
||||||
setTypes(
|
setTypes(
|
||||||
Object.keys(result.data).reduce((acc, curr) => {
|
// Reverse the keys so the tool world does not overlap
|
||||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
Object.keys(result.data)
|
||||||
|
.reverse()
|
||||||
|
.reduce((acc, curr) => {
|
||||||
|
Object.keys(result.data[curr]).forEach(
|
||||||
|
(c: keyof APIKindType) => {
|
||||||
acc[c] = curr;
|
acc[c] = curr;
|
||||||
// Add the base classes to the accumulator as well.
|
// Add the base classes to the accumulator as well.
|
||||||
result.data[curr][c].base_classes?.forEach((b) => {
|
result.data[curr][c].base_classes?.forEach((b) => {
|
||||||
acc[b] = curr;
|
acc[b] = curr;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,19 @@
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { Edge, Node, useReactFlow } from "reactflow";
|
import { Edge, Node, useReactFlow } from "reactflow";
|
||||||
import { TabsContext } from "../../../contexts/tabsContext";
|
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
import { TabsContext } from "./tabsContext";
|
||||||
|
|
||||||
|
type undoRedoContextType = {
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
takeSnapshot: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
type UseUndoRedoOptions = {
|
type UseUndoRedoOptions = {
|
||||||
maxHistorySize: number;
|
maxHistorySize: number;
|
||||||
|
|
@ -21,27 +33,34 @@ type HistoryItem = {
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initialValue = {
|
||||||
|
undo: () => {},
|
||||||
|
redo: () => {},
|
||||||
|
takeSnapshot: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
const defaultOptions: UseUndoRedoOptions = {
|
const defaultOptions: UseUndoRedoOptions = {
|
||||||
maxHistorySize: 100,
|
maxHistorySize: 100,
|
||||||
enableShortcuts: true,
|
enableShortcuts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://redux.js.org/usage/implementing-undo-history
|
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
|
||||||
export const useUndoRedo: UseUndoRedo = ({
|
|
||||||
maxHistorySize = defaultOptions.maxHistorySize,
|
export function UndoRedoProvider({ children }) {
|
||||||
enableShortcuts = defaultOptions.enableShortcuts,
|
const { tabId, flows } = useContext(TabsContext);
|
||||||
} = defaultOptions) => {
|
|
||||||
// the past and future arrays store the states that we can jump to
|
|
||||||
const { tabIndex, flows } = useContext(TabsContext);
|
|
||||||
|
|
||||||
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||||
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||||
|
const [tabIndex, setTabIndex] = useState(
|
||||||
|
flows.findIndex((f) => f.id === tabId)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// whenever the flows variable changes, we need to add one array to the past and future states
|
// whenever the flows variable changes, we need to add one array to the past and future states
|
||||||
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
||||||
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
||||||
}, [flows]);
|
setTabIndex(flows.findIndex((f) => f.id === tabId));
|
||||||
|
}, [flows, tabId]);
|
||||||
|
|
||||||
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
|
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
|
||||||
|
|
||||||
|
|
@ -50,7 +69,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
||||||
setPast((old) => {
|
setPast((old) => {
|
||||||
let newPast = cloneDeep(old);
|
let newPast = cloneDeep(old);
|
||||||
newPast[tabIndex] = old[tabIndex].slice(
|
newPast[tabIndex] = old[tabIndex].slice(
|
||||||
old[tabIndex].length - maxHistorySize + 1,
|
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
|
||||||
old[tabIndex].length
|
old[tabIndex].length
|
||||||
);
|
);
|
||||||
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
||||||
|
|
@ -63,16 +82,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
||||||
newFuture[tabIndex] = [];
|
newFuture[tabIndex] = [];
|
||||||
return newFuture;
|
return newFuture;
|
||||||
});
|
});
|
||||||
}, [
|
}, [getNodes, getEdges, past, future, flows, tabId, setPast, setFuture]);
|
||||||
getNodes,
|
|
||||||
getEdges,
|
|
||||||
past,
|
|
||||||
future,
|
|
||||||
tabIndex,
|
|
||||||
setPast,
|
|
||||||
setFuture,
|
|
||||||
maxHistorySize,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const undo = useCallback(() => {
|
const undo = useCallback(() => {
|
||||||
// get the last state that we want to go back to
|
// get the last state that we want to go back to
|
||||||
|
|
@ -141,7 +151,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// this effect is used to attach the global event handlers
|
// this effect is used to attach the global event handlers
|
||||||
if (!enableShortcuts) {
|
if (!defaultOptions.enableShortcuts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,15 +175,16 @@ export const useUndoRedo: UseUndoRedo = ({
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", keyDownHandler);
|
document.removeEventListener("keydown", keyDownHandler);
|
||||||
};
|
};
|
||||||
}, [undo, redo, enableShortcuts]);
|
}, [undo, redo]);
|
||||||
|
return (
|
||||||
return {
|
<undoRedoContext.Provider
|
||||||
|
value={{
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
takeSnapshot,
|
takeSnapshot,
|
||||||
canUndo: !!past.length,
|
}}
|
||||||
canRedo: !!future.length,
|
>
|
||||||
};
|
{children}
|
||||||
};
|
</undoRedoContext.Provider>
|
||||||
|
);
|
||||||
export default useUndoRedo;
|
}
|
||||||
|
|
@ -1,19 +1,29 @@
|
||||||
import { PromptTypeAPI, errorsTypeAPI } from "./../../types/api/index";
|
import {
|
||||||
|
BuildStatusTypeAPI,
|
||||||
|
PromptTypeAPI,
|
||||||
|
errorsTypeAPI,
|
||||||
|
InitTypeAPI,
|
||||||
|
} from "./../../types/api/index";
|
||||||
import { APIObjectType, sendAllProps } from "../../types/api/index";
|
import { APIObjectType, sendAllProps } from "../../types/api/index";
|
||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { FlowType } from "../../types/flow";
|
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||||
|
import { ReactFlowJsonObject } from "reactflow";
|
||||||
// when serving with static files
|
|
||||||
// We need to add /api/v1/ to the url in the axios calls
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all data from the API.
|
* Fetches all objects from the API endpoint.
|
||||||
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse object containing the API data.
|
*
|
||||||
|
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
|
||||||
*/
|
*/
|
||||||
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
|
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
|
||||||
return await axios.get(`/api/v1/all`);
|
return await axios.get(`/api/v1/all`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends data to the API for prediction.
|
||||||
|
*
|
||||||
|
* @param {sendAllProps} data - The data to be sent to the API.
|
||||||
|
* @returns {AxiosResponse<any>} The API response.
|
||||||
|
*/
|
||||||
export async function sendAll(data: sendAllProps) {
|
export async function sendAll(data: sendAllProps) {
|
||||||
return await axios.post(`/api/v1/predict`, data);
|
return await axios.post(`/api/v1/predict`, data);
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +41,12 @@ export async function postValidateNode(
|
||||||
return await axios.post(`/api/v1/validate/node/${nodeId}`, { data });
|
return await axios.post(`/api/v1/validate/node/${nodeId}`, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the prompt for the code block by sending it to an API endpoint.
|
||||||
|
*
|
||||||
|
* @param {string} template - The template string of the prompt to check.
|
||||||
|
* @returns {Promise<AxiosResponse<PromptTypeAPI>>} A promise that resolves to an AxiosResponse containing the validation results.
|
||||||
|
*/
|
||||||
export async function checkPrompt(
|
export async function checkPrompt(
|
||||||
template: string
|
template: string
|
||||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||||
|
|
@ -38,19 +54,10 @@ export async function checkPrompt(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the version of the API.
|
* Fetches a list of JSON files from a GitHub repository and returns their contents as an array of FlowType objects.
|
||||||
* @returns {Promise<AxiosResponse<{ version: string }>>} A promise that resolves to an AxiosResponse object containing the API version.
|
*
|
||||||
* @example
|
* @returns {Promise<FlowType[]>} A promise that resolves to an array of FlowType objects.
|
||||||
* const response = await getVersion();
|
|
||||||
* console.log(response.data.version);
|
|
||||||
* // 0.1.0
|
|
||||||
*/
|
*/
|
||||||
export async function getVersion(): Promise<
|
|
||||||
AxiosResponse<{ version: string }>
|
|
||||||
> {
|
|
||||||
return await axios.get("/api/v1/version");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getExamples(): Promise<FlowType[]> {
|
export async function getExamples(): Promise<FlowType[]> {
|
||||||
const url =
|
const url =
|
||||||
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples";
|
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples";
|
||||||
|
|
@ -67,3 +74,234 @@ export async function getExamples(): Promise<FlowType[]> {
|
||||||
|
|
||||||
return await Promise.all(contentsPromises);
|
return await Promise.all(contentsPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a new flow to the database.
|
||||||
|
*
|
||||||
|
* @param {FlowType} newFlow - The flow data to save.
|
||||||
|
* @returns {Promise<any>} The saved flow data.
|
||||||
|
* @throws Will throw an error if saving fails.
|
||||||
|
*/
|
||||||
|
export async function saveFlowToDatabase(newFlow: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
data: ReactFlowJsonObject;
|
||||||
|
description: string;
|
||||||
|
style?: FlowStyleType;
|
||||||
|
}): Promise<FlowType> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/v1/flows/", {
|
||||||
|
name: newFlow.name,
|
||||||
|
data: newFlow.data,
|
||||||
|
description: newFlow.description,
|
||||||
|
});
|
||||||
|
if (response.status !== 201) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Updates an existing flow in the database.
|
||||||
|
*
|
||||||
|
* @param {FlowType} updatedFlow - The updated flow data.
|
||||||
|
* @returns {Promise<any>} The updated flow data.
|
||||||
|
* @throws Will throw an error if the update fails.
|
||||||
|
*/
|
||||||
|
export async function updateFlowInDatabase(
|
||||||
|
updatedFlow: FlowType
|
||||||
|
): Promise<FlowType> {
|
||||||
|
try {
|
||||||
|
const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, {
|
||||||
|
name: updatedFlow.name,
|
||||||
|
data: updatedFlow.data,
|
||||||
|
description: updatedFlow.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all flows from the database.
|
||||||
|
*
|
||||||
|
* @returns {Promise<any>} The flows data.
|
||||||
|
* @throws Will throw an error if reading fails.
|
||||||
|
*/
|
||||||
|
export async function readFlowsFromDatabase() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/v1/flows/");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadFlowsFromDatabase() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/v1/flows/download/");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFlowsToDatabase(flows) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/api/v1/flows/upload/`, flows);
|
||||||
|
|
||||||
|
if (response.status !== 201) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a flow from the database.
|
||||||
|
*
|
||||||
|
* @param {string} flowId - The ID of the flow to delete.
|
||||||
|
* @returns {Promise<any>} The deleted flow data.
|
||||||
|
* @throws Will throw an error if deletion fails.
|
||||||
|
*/
|
||||||
|
export async function deleteFlowFromDatabase(flowId: string) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/v1/flows/${flowId}`);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a flow from the database by ID.
|
||||||
|
*
|
||||||
|
* @param {number} flowId - The ID of the flow to fetch.
|
||||||
|
* @returns {Promise<any>} The flow data.
|
||||||
|
* @throws Will throw an error if fetching fails.
|
||||||
|
*/
|
||||||
|
export async function getFlowFromDatabase(flowId: number) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/v1/flows/${flowId}`);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches flow styles from the database.
|
||||||
|
*
|
||||||
|
* @returns {Promise<any>} The flow styles data.
|
||||||
|
* @throws Will throw an error if fetching fails.
|
||||||
|
*/
|
||||||
|
export async function getFlowStylesFromDatabase() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/v1/flow_styles/");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a new flow style to the database.
|
||||||
|
*
|
||||||
|
* @param {FlowStyleType} flowStyle - The flow style data to save.
|
||||||
|
* @returns {Promise<any>} The saved flow style data.
|
||||||
|
* @throws Will throw an error if saving fails.
|
||||||
|
*/
|
||||||
|
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/v1/flow_styles/", flowStyle, {
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 201) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the version of the API.
|
||||||
|
*
|
||||||
|
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information.
|
||||||
|
*/
|
||||||
|
export async function getVersion() {
|
||||||
|
const respnose = await axios.get("/api/v1/version");
|
||||||
|
return respnose.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the health status of the API.
|
||||||
|
*
|
||||||
|
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status.
|
||||||
|
*/
|
||||||
|
export async function getHealth() {
|
||||||
|
return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the build status of a flow.
|
||||||
|
* @param {string} flowId - The ID of the flow to fetch the build status for.
|
||||||
|
* @returns {Promise<BuildStatusTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export async function getBuildStatus(
|
||||||
|
flowId: string
|
||||||
|
): Promise<BuildStatusTypeAPI> {
|
||||||
|
return await axios.get(`/api/v1/build/${flowId}/status`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//docs for postbuildinit
|
||||||
|
/**
|
||||||
|
* Posts the build init of a flow.
|
||||||
|
* @param {string} flowId - The ID of the flow to fetch the build status for.
|
||||||
|
* @returns {Promise<InitTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export async function postBuildInit(
|
||||||
|
flow: FlowType
|
||||||
|
): Promise<AxiosResponse<InitTypeAPI>> {
|
||||||
|
return await axios.post(`/api/v1/build/init`, flow);
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
|
@ -4,115 +4,134 @@
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||||
--foreground: 222.2 47.4% 11.2%;
|
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||||
--popover-foreground: 222.2 47.4% 11.2%;
|
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||||
--card-foreground: 222.2 47.4% 11.2%;
|
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
|
|
||||||
--destructive: 0 100% 50%;
|
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
|
|
||||||
--ring: 215 20.2% 65.1%;
|
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 224 71% 4%;
|
--background: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||||
--foreground: 213 31% 91%;
|
--foreground: 213 31% 91%; /* hsl(213 31% 91%) */
|
||||||
|
|
||||||
--muted: 223 47% 11%;
|
--muted: 223 47% 11%; /* hsl(223 47% 11%) */
|
||||||
--muted-foreground: 215.4 16.3% 56.9%;
|
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
|
||||||
|
|
||||||
--popover: 224 71% 4%;
|
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||||
--popover-foreground: 215 20.2% 65.1%;
|
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||||
|
|
||||||
--card: 224 71% 4%;
|
--card: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||||
--card-foreground: 213 31% 91%;
|
--card-foreground: 213 31% 91%; /* hsl(213 31% 91%) */
|
||||||
|
|
||||||
--border: 216 34% 17%;
|
--border: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||||
--input: 216 34% 17%;
|
--input: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
--primary-foreground: 222.2 47.4% 1.2%;
|
--primary-foreground: 222.2 47.4% 1.2%; /* hsl(222 47% 1%) */
|
||||||
|
|
||||||
--secondary: 222.2 47.4% 11.2%;
|
--secondary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
|
|
||||||
--accent: 216 34% 17%;
|
--accent: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
|
|
||||||
--destructive: 0 63% 31%;
|
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
|
|
||||||
--ring: 216 34% 17%;
|
--ring: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||||
--foreground: 222.2 47.4% 11.2%;
|
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||||
--popover-foreground: 222.2 47.4% 11.2%;
|
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||||
--card-foreground: 222.2 47.4% 11.2%;
|
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||||
--destructive: 0 100% 50%;
|
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||||
--ring: 215 20.2% 65.1%;
|
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
-background: 224 71% 4%;
|
-background: 224 71% 4%;
|
||||||
|
/* hsl(224 71% 4%) */
|
||||||
-foreground: 213 31% 91%;
|
-foreground: 213 31% 91%;
|
||||||
|
/* hsl(213 31% 91%) */
|
||||||
-muted: 223 47% 11%;
|
-muted: 223 47% 11%;
|
||||||
|
/* hsl(223 47% 11%) */
|
||||||
-muted-foreground: 215.4 16.3% 56.9%;
|
-muted-foreground: 215.4 16.3% 56.9%;
|
||||||
|
/* hsl(215 16% 56%) */
|
||||||
-popover: 224 71% 4%;
|
-popover: 224 71% 4%;
|
||||||
|
/* hsl(224 71% 4%) */
|
||||||
-popover-foreground: 215 20.2% 65.1%;
|
-popover-foreground: 215 20.2% 65.1%;
|
||||||
|
/* hsl(215 20% 65%) */
|
||||||
-card: 224 71% 4%;
|
-card: 224 71% 4%;
|
||||||
|
/* hsl(224 71% 4%) */
|
||||||
-card-foreground: 213 31% 91%;
|
-card-foreground: 213 31% 91%;
|
||||||
|
/* hsl(213 31% 91%) */
|
||||||
-border: 216 34% 17%;
|
-border: 216 34% 17%;
|
||||||
|
/* hsl(216 34% 17%) */
|
||||||
-input: 216 34% 17%;
|
-input: 216 34% 17%;
|
||||||
|
/* hsl(216 34% 17%) */
|
||||||
-primary: 210 40% 98%;
|
-primary: 210 40% 98%;
|
||||||
|
/* hsl(210 40% 98%) */
|
||||||
-primary-foreground: 222.2 47.4% 1.2%;
|
-primary-foreground: 222.2 47.4% 1.2%;
|
||||||
|
/* hsl(222 47% 1%) */
|
||||||
-secondary: 222.2 47.4% 11.2%;
|
-secondary: 222.2 47.4% 11.2%;
|
||||||
|
/* hsl(222 47% 11%) */
|
||||||
-secondary-foreground: 210 40% 98%;
|
-secondary-foreground: 210 40% 98%;
|
||||||
|
/* hsl(210 40% 98%) */
|
||||||
-accent: 216 34% 17%;
|
-accent: 216 34% 17%;
|
||||||
|
/* hsl(216 34% 17%) */
|
||||||
-accent-foreground: 210 40% 98%;
|
-accent-foreground: 210 40% 98%;
|
||||||
|
/* hsl(210 40% 98%) */
|
||||||
-destructive: 0 63% 31%;
|
-destructive: 0 63% 31%;
|
||||||
|
/* hsl(0 63% 31%) */
|
||||||
-destructive-foreground: 210 40% 98%;
|
-destructive-foreground: 210 40% 98%;
|
||||||
|
/* hsl(210 40% 98%) */
|
||||||
-ring: 216 34% 17%;
|
-ring: 216 34% 17%;
|
||||||
|
/* hsl(216 34% 17%) */
|
||||||
-radius: 0.5rem;
|
-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
import { CodeBracketSquareIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import { useContext, useState } from "react";
|
||||||
XMarkIcon,
|
|
||||||
CommandLineIcon,
|
|
||||||
CodeBracketSquareIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { Fragment, useContext, useRef, useState } from "react";
|
|
||||||
import { PopUpContext } from "../../contexts/popUpContext";
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import "ace-builds/src-noconflict/mode-python";
|
import "ace-builds/src-noconflict/mode-python";
|
||||||
import "ace-builds/src-noconflict/theme-github";
|
import "ace-builds/src-noconflict/theme-github";
|
||||||
|
|
@ -18,18 +13,26 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "../../components/ui/dialog";
|
} from "../../components/ui/dialog";
|
||||||
import { Button } from "../../components/ui/button";
|
import { FlowType } from "../../types/flow/index";
|
||||||
|
import { getCurlCode, getPythonApiCode, getPythonCode } from "../../constants";
|
||||||
|
import { EXPORT_CODE_DIALOG } from "../../constants";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "../../components/ui/tabs";
|
||||||
|
import { Check, Clipboard } from "lucide-react";
|
||||||
|
|
||||||
export default function ApiModal({ flowName }) {
|
export default function ApiModal({ flow }: { flow: FlowType }) {
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
const { dark } = useContext(darkContext);
|
const { dark } = useContext(darkContext);
|
||||||
const { closePopUp } = useContext(PopUpContext);
|
const { closePopUp } = useContext(PopUpContext);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState("0");
|
||||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
|
|
@ -52,27 +55,18 @@ export default function ApiModal({ flowName }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pythonApiCode = `import requests
|
const pythonApiCode = getPythonApiCode(flow);
|
||||||
import json
|
|
||||||
|
|
||||||
API_URL = "${window.location.protocol}//${window.location.host}/predict"
|
const curl_code = getCurlCode(flow);
|
||||||
|
const pythonCode = getPythonCode(flow);
|
||||||
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 = [
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: "cURL",
|
||||||
|
mode: "bash",
|
||||||
|
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||||
|
code: curl_code,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Python API",
|
name: "Python API",
|
||||||
mode: "python",
|
mode: "python",
|
||||||
|
|
@ -90,7 +84,7 @@ flow("Hey, have you heard of LangFlow?")`;
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||||
<DialogTrigger></DialogTrigger>
|
<DialogTrigger></DialogTrigger>
|
||||||
<DialogContent className="lg:max-w-[800px] sm:max-w-[600px] h-[550px]">
|
<DialogContent className="lg:max-w-[800px] sm:max-w-[600px] h-[580px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center">
|
<DialogTitle className="flex items-center">
|
||||||
<span className="pr-2">Code</span>
|
<span className="pr-2">Code</span>
|
||||||
|
|
@ -99,57 +93,46 @@ flow("Hey, have you heard of LangFlow?")`;
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{EXPORT_CODE_DIALOG}</DialogDescription>
|
||||||
Export your flow to use it with this code.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col h-full w-full ">
|
<Tabs
|
||||||
<div className="flex px-5 z-10">
|
defaultValue={"0"}
|
||||||
{tabs.map((tab, index) => (
|
className="w-full h-full overflow-hidden text-center bg-muted rounded-md border"
|
||||||
<button
|
onValueChange={(value) => setActiveTab(value)}
|
||||||
key={index}
|
|
||||||
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}
|
<div className="flex items-center justify-between px-2">
|
||||||
<img src={tab.image} className="w-6" />
|
<TabsList>
|
||||||
</button>
|
{tabs.map((tab, index) => (
|
||||||
|
<TabsTrigger value={index.toString()}>{tab.name}</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</div>
|
</TabsList>
|
||||||
<div className="overflow-hidden px-4 sm:p-4 sm:pb-0 sm:pt-0 w-full h-full rounded-lg shadow bg-white dark:bg-gray-800">
|
|
||||||
<div className="items-center mb-2">
|
|
||||||
<div className="float-right">
|
<div className="float-right">
|
||||||
<button
|
<button
|
||||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? <Check size={18} /> : <Clipboard size={15} />}
|
||||||
<IconCheck size={18} />
|
|
||||||
) : (
|
|
||||||
<IconClipboard size={18} />
|
|
||||||
)}
|
|
||||||
{isCopied ? "Copied!" : "Copy code"}
|
{isCopied ? "Copied!" : "Copy code"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SyntaxHighlighter
|
|
||||||
className="h-[370px] w-full"
|
{tabs.map((tab, index) => (
|
||||||
language={tabs[activeTab].mode}
|
<TabsContent
|
||||||
style={oneDark}
|
value={index.toString()}
|
||||||
customStyle={{ margin: 0 }}
|
className="overflow-hidden w-full h-full px-4 pb-4 -mt-1"
|
||||||
>
|
>
|
||||||
{tabs[activeTab].code}
|
<SyntaxHighlighter
|
||||||
|
className="h-[400px] w-full overflow-auto"
|
||||||
|
language={tab.mode}
|
||||||
|
style={oneDark}
|
||||||
|
>
|
||||||
|
{tab.code}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</div>
|
</TabsContent>
|
||||||
</div>
|
))}
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,27 @@
|
||||||
import {
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
ChevronDoubleLeftIcon,
|
|
||||||
ChevronDoubleRightIcon,
|
|
||||||
PencilSquareIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
|
||||||
import { PopUpContext } from "../../contexts/popUpContext";
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import { NodeDataType } from "../../types/flow";
|
import { NodeDataType } from "../../types/flow";
|
||||||
import { classNames, limitScrollFieldsModal, nodeIcons } from "../../utils";
|
import { classNames, limitScrollFieldsModal } from "../../utils";
|
||||||
import { typesContext } from "../../contexts/typesContext";
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
import { Switch } from "../../components/ui/switch";
|
|
||||||
import ToggleShadComponent from "../../components/toggleShadComponent";
|
import ToggleShadComponent from "../../components/toggleShadComponent";
|
||||||
import { VariableIcon } from "@heroicons/react/24/outline";
|
import { VariableIcon } from "@heroicons/react/24/outline";
|
||||||
import InputListComponent from "../../components/inputListComponent";
|
import InputListComponent from "../../components/inputListComponent";
|
||||||
import TextAreaComponent from "../../components/textAreaComponent";
|
import TextAreaComponent from "../../components/textAreaComponent";
|
||||||
import InputComponent from "../../components/inputComponent";
|
import InputComponent from "../../components/inputComponent";
|
||||||
import ToggleComponent from "../../components/toggleComponent";
|
|
||||||
import FloatComponent from "../../components/floatComponent";
|
import FloatComponent from "../../components/floatComponent";
|
||||||
import Dropdown from "../../components/dropdownComponent";
|
import Dropdown from "../../components/dropdownComponent";
|
||||||
import IntComponent from "../../components/intComponent";
|
import IntComponent from "../../components/intComponent";
|
||||||
import InputFileComponent from "../../components/inputFileComponent";
|
import InputFileComponent from "../../components/inputFileComponent";
|
||||||
import PromptAreaComponent from "../../components/promptComponent";
|
import PromptAreaComponent from "../../components/promptComponent";
|
||||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -42,7 +32,7 @@ import {
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "../../components/ui/dialog";
|
} from "../../components/ui/dialog";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { EDIT_DIALOG_SUBTITLE } from "../../constants";
|
import { Badge } from "../../components/ui/badge";
|
||||||
|
|
||||||
export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
|
|
@ -60,12 +50,11 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
data.node.template[t].type === "int")
|
data.node.template[t].type === "int")
|
||||||
).length
|
).length
|
||||||
);
|
);
|
||||||
const [nodeValue, setNodeValue] = useState(true);
|
const [nodeValue, setNodeValue] = useState(null);
|
||||||
const { closePopUp } = useContext(PopUpContext);
|
const { closePopUp } = useContext(PopUpContext);
|
||||||
const { types } = useContext(typesContext);
|
const { types } = useContext(typesContext);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const { save } = useContext(TabsContext);
|
const [enabled, setEnabled] = useState(null);
|
||||||
const [enabled, setEnabled] = useState(false);
|
|
||||||
if (nodeLength == 0) {
|
if (nodeLength == 0) {
|
||||||
closePopUp();
|
closePopUp();
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +66,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||||
|
|
||||||
function changeAdvanced(node): void {
|
function changeAdvanced(node): void {
|
||||||
Object.keys(data.node.template).filter((n, i) => {
|
Object.keys(data.node.template).filter((n, i) => {
|
||||||
if (n === node.name) {
|
if (n === node.name) {
|
||||||
|
|
@ -87,37 +78,32 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
setNodeValue(!nodeValue);
|
setNodeValue(!nodeValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(data.node.template);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||||
<DialogTrigger></DialogTrigger>
|
<DialogTrigger></DialogTrigger>
|
||||||
<DialogContent className="lg:max-w-[700px] ">
|
<DialogContent className="lg:max-w-[700px] ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center">
|
<DialogTitle className="flex items-center">
|
||||||
<span className="pr-2">Edit Node</span>
|
<span className="pr-2">{data.type}</span>
|
||||||
<PencilSquareIcon
|
<Badge variant="secondary">ID: {data.id}</Badge>
|
||||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{EDIT_DIALOG_SUBTITLE}
|
{data.node?.description}
|
||||||
<div className="flex pt-3">
|
<div className="flex pt-4">
|
||||||
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2">
|
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2 dark:text-slate-200">
|
||||||
|
|
||||||
</VariableIcon>
|
</VariableIcon>
|
||||||
<span className="text-sm font-semibold text-gray-800">
|
<span className="text-sm font-semibold text-gray-800 dark:text-white">
|
||||||
Parameters
|
Parameters
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex w-full h-fit max-h-[415px]">
|
<div className="flex w-full max-h-[350px] h-fit">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full rounded-lg bg-white dark:bg-gray-800 shadow",
|
"w-full rounded-lg bg-white dark:bg-gray-800 border-[1px] border-gray-200",
|
||||||
nodeLength > limitScrollFieldsModal
|
nodeLength > limitScrollFieldsModal
|
||||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||||
: "overflow-hidden"
|
: "overflow-hidden"
|
||||||
|
|
@ -125,9 +111,9 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
>
|
>
|
||||||
{nodeLength > 0 && (
|
{nodeLength > 0 && (
|
||||||
<div className="flex flex-col gap-5 h-fit">
|
<div className="flex flex-col gap-5 h-fit">
|
||||||
<Table className="table-fixed">
|
<Table className="table-fixed bg-muted outline-1">
|
||||||
<TableHeader className="border-gray-200 text-gray-500 text-xs font-medium">
|
<TableHeader className="border-gray-200 text-gray-500 text-xs font-medium h-10">
|
||||||
<TableRow>
|
<TableRow className="dark:border-b-muted">
|
||||||
<TableHead className="h-7 text-center">PARAM</TableHead>
|
<TableHead className="h-7 text-center">PARAM</TableHead>
|
||||||
<TableHead className="p-0 h-7 text-center">
|
<TableHead className="p-0 h-7 text-center">
|
||||||
VALUE
|
VALUE
|
||||||
|
|
@ -150,8 +136,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
data.node.template[t].type === "int")
|
data.node.template[t].type === "int")
|
||||||
)
|
)
|
||||||
.map((n, i) => (
|
.map((n, i) => (
|
||||||
<TableRow key={i} className="h-8">
|
<TableRow key={i} className="h-10 dark:border-b-muted">
|
||||||
<TableCell className="p-0 text-center text-gray-900 text-xs dark:text-gray-300">
|
<TableCell className="p-0 text-center text-gray-900 dark:text-gray-300 text-sm">
|
||||||
{data.node.template[n].name
|
{data.node.template[n].name
|
||||||
? data.node.template[n].name
|
? data.node.template[n].name
|
||||||
: data.node.template[n].display_name}
|
: data.node.template[n].display_name}
|
||||||
|
|
@ -162,6 +148,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
{data.node.template[n].list ? (
|
{data.node.template[n].list ? (
|
||||||
<InputListComponent
|
<InputListComponent
|
||||||
|
editNode={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
value={
|
value={
|
||||||
!data.node.template[n].value ||
|
!data.node.template[n].value ||
|
||||||
|
|
@ -171,7 +158,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
}
|
}
|
||||||
onChange={(t: string[]) => {
|
onChange={(t: string[]) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : data.node.template[n].multiline ? (
|
) : data.node.template[n].multiline ? (
|
||||||
|
|
@ -181,7 +167,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
value={data.node.template[n].value ?? ""}
|
value={data.node.template[n].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={(t: string) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -194,7 +179,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
value={data.node.template[n].value ?? ""}
|
value={data.node.template[n].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={(t) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -207,8 +191,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
setEnabled={(e) => {
|
setEnabled={(e) => {
|
||||||
data.node.template[n].value = e;
|
data.node.template[n].value = e;
|
||||||
setEnabled(e);
|
setEnabled(e);
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -220,7 +204,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
value={data.node.template[n].value ?? ""}
|
value={data.node.template[n].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={(t) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,6 +211,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
data.node.template[n].options ? (
|
data.node.template[n].options ? (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
numberOfOptions={nodeLength}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
options={data.node.template[n].options}
|
options={data.node.template[n].options}
|
||||||
onSelect={(newValue) =>
|
onSelect={(newValue) =>
|
||||||
|
|
@ -247,7 +231,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
value={data.node.template[n].value ?? ""}
|
value={data.node.template[n].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={(t) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,7 +247,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
suffixes={data.node.template[n].suffixes}
|
suffixes={data.node.template[n].suffixes}
|
||||||
onFileChange={(t: string) => {
|
onFileChange={(t: string) => {
|
||||||
data.node.template[n].content = t;
|
data.node.template[n].content = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
></InputFileComponent>
|
></InputFileComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -276,7 +258,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
value={data.node.template[n].value ?? ""}
|
value={data.node.template[n].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={(t: string) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -288,7 +269,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
value={data.node.template[n].value ?? ""}
|
value={data.node.template[n].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={(t: string) => {
|
||||||
data.node.template[n].value = t;
|
data.node.template[n].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -306,6 +286,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
changeAdvanced(data.node.template[n])
|
changeAdvanced(data.node.template[n])
|
||||||
}
|
}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -326,7 +307,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||||
}}
|
}}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ export default function ModalField({
|
||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
}) {
|
}) {
|
||||||
const { save } = useContext(TabsContext);
|
|
||||||
const [enabled, setEnabled] = useState(
|
const [enabled, setEnabled] = useState(
|
||||||
data.node.template[name]?.value ?? false
|
data.node.template[name]?.value ?? false
|
||||||
);
|
);
|
||||||
|
|
@ -71,7 +70,6 @@ export default function ModalField({
|
||||||
}
|
}
|
||||||
onChange={(t: string[]) => {
|
onChange={(t: string[]) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : data.node.template[name].multiline ? (
|
) : data.node.template[name].multiline ? (
|
||||||
|
|
@ -80,7 +78,6 @@ export default function ModalField({
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={(t: string) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -90,7 +87,6 @@ export default function ModalField({
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={(t) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -104,7 +100,6 @@ export default function ModalField({
|
||||||
setEnabled={(t) => {
|
setEnabled={(t) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
setEnabled(t);
|
setEnabled(t);
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,7 +110,6 @@ export default function ModalField({
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={(t) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -134,7 +128,6 @@ export default function ModalField({
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={(t) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -150,7 +143,6 @@ export default function ModalField({
|
||||||
suffixes={data.node.template[name].suffixes}
|
suffixes={data.node.template[name].suffixes}
|
||||||
onFileChange={(t: string) => {
|
onFileChange={(t: string) => {
|
||||||
data.node.template[name].content = t;
|
data.node.template[name].content = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
></InputFileComponent>
|
></InputFileComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -161,7 +153,6 @@ export default function ModalField({
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={(t: string) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -172,7 +163,6 @@ export default function ModalField({
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={(t: string) => {
|
||||||
data.node.template[name].value = t;
|
data.node.template[name].value = t;
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export default function NodeModal({ data }: { data: NodeDataType }) {
|
||||||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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-1 focus:ring-indigo-500 focus:ring-offset-1 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||||
import { classNames } from "../../../utils";
|
import { classNames } from "../../../utils";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { TabsContext } from "../../../contexts/tabsContext";
|
import { TabsContext } from "../../../contexts/tabsContext";
|
||||||
|
import { INPUT_STYLE } from "../../../constants";
|
||||||
export default function ChatInput({
|
export default function ChatInput({
|
||||||
lockChat,
|
lockChat,
|
||||||
chatValue,
|
chatValue,
|
||||||
|
|
@ -44,9 +44,10 @@ export default function ChatInput({
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
lockChat
|
lockChat
|
||||||
? " bg-gray-300 text-black dark:bg-gray-700 dark:text-gray-300"
|
? " bg-input text-black dark:bg-gray-700 dark:text-gray-300"
|
||||||
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
|
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
|
||||||
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm"
|
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" +
|
||||||
|
INPUT_STYLE
|
||||||
)}
|
)}
|
||||||
placeholder={"Send a message..."}
|
placeholder={"Send a message..."}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ export default function ChatMessage({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full py-2 pl-2 flex",
|
"w-full py-2 pl-2 flex",
|
||||||
chat.isSend
|
chat.isSend
|
||||||
? "bg-white dark:bg-gray-900 "
|
? "bg-background dark:bg-gray-900 "
|
||||||
: "bg-gray-200 dark:bg-gray-800"
|
: "bg-input dark:bg-gray-800"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -80,7 +80,7 @@ export default function ChatMessage({
|
||||||
<div
|
<div
|
||||||
onClick={() => setHidden((prev) => !prev)}
|
onClick={() => setHidden((prev) => !prev)}
|
||||||
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
|
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
|
||||||
bg-gray-100 dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
|
bg-muted dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: convert.toHtml(chat.thought),
|
__html: convert.toHtml(chat.thought),
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default function FileCard({ fileName, content, fileType }) {
|
||||||
/>
|
/>
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 right-0 bg-gray-100 text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
|
className={`absolute top-0 right-0 bg-muted text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
|
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
|
@ -54,7 +54,7 @@ export default function FileCard({ fileName, content, fileType }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
className="bg-gray-100 shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
|
className="bg-muted shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 text-current items-center w-full mr-2">
|
<div className="flex gap-2 text-current items-center w-full mr-2">
|
||||||
{" "}
|
{" "}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {
|
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||||
ChatBubbleOvalLeftEllipsisIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { FlowType, NodeType } from "../../types/flow";
|
import { FlowType } from "../../types/flow";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { toNormalCase } from "../../utils";
|
import { validateNodes } from "../../utils";
|
||||||
import { typesContext } from "../../contexts/typesContext";
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
import ChatMessage from "./chatMessage";
|
import ChatMessage from "./chatMessage";
|
||||||
import { FaEraser } from "react-icons/fa";
|
import { Eraser } from "lucide-react";
|
||||||
import { HiX } from "react-icons/hi";
|
import { X } from "lucide-react";
|
||||||
import { sendAllProps } from "../../types/api";
|
import { sendAllProps } from "../../types/api";
|
||||||
import { ChatMessageType, ChatType } from "../../types/chat";
|
import { ChatMessageType } from "../../types/chat";
|
||||||
import ChatInput from "./chatInput";
|
import ChatInput from "./chatInput";
|
||||||
|
|
||||||
import _, { set } from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
export default function ChatModal({
|
export default function ChatModal({
|
||||||
flow,
|
flow,
|
||||||
|
|
@ -114,6 +111,17 @@ export default function ChatModal({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWebSocketUrl(chatId, isDevelopment = false) {
|
||||||
|
const isSecureProtocol = window.location.protocol === "https:";
|
||||||
|
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
|
||||||
|
const host = isDevelopment ? "localhost:7860" : window.location.host;
|
||||||
|
const chatEndpoint = `/api/v1/chat/${chatId}`;
|
||||||
|
|
||||||
|
return `${
|
||||||
|
isDevelopment ? "ws" : webSocketProtocol
|
||||||
|
}://${host}${chatEndpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
function handleWsMessage(data: any) {
|
function handleWsMessage(data: any) {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
//set chat history
|
//set chat history
|
||||||
|
|
@ -180,12 +188,10 @@ export default function ChatModal({
|
||||||
|
|
||||||
function connectWS() {
|
function connectWS() {
|
||||||
try {
|
try {
|
||||||
const urlWs =
|
const urlWs = getWebSocketUrl(
|
||||||
|
id.current,
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? `ws://localhost:7860/api/v1/chat/${id.current}`
|
);
|
||||||
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${
|
|
||||||
window.location.host
|
|
||||||
}api/v1/chat/${id.current}`;
|
|
||||||
const newWs = new WebSocket(urlWs);
|
const newWs = new WebSocket(urlWs);
|
||||||
newWs.onopen = () => {
|
newWs.onopen = () => {
|
||||||
console.log("WebSocket connection established!");
|
console.log("WebSocket connection established!");
|
||||||
|
|
@ -264,54 +270,6 @@ export default function ChatModal({
|
||||||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [chatHistory]);
|
}, [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 === undefined ||
|
|
||||||
template[t].value === null ||
|
|
||||||
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
|
|
||||||
: toNormalCase(template[t].name)
|
|
||||||
}.`,
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
[] as string[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateNodes() {
|
|
||||||
return reactFlowInstance
|
|
||||||
.getNodes()
|
|
||||||
.flatMap((n: NodeType) => validateNode(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -322,7 +280,7 @@ export default function ChatModal({
|
||||||
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
if (chatValue !== "") {
|
if (chatValue !== "") {
|
||||||
let nodeValidationErrors = validateNodes();
|
let nodeValidationErrors = validateNodes(reactFlowInstance);
|
||||||
if (nodeValidationErrors.length === 0) {
|
if (nodeValidationErrors.length === 0) {
|
||||||
setLockChat(true);
|
setLockChat(true);
|
||||||
let message = chatValue;
|
let message = chatValue;
|
||||||
|
|
@ -394,13 +352,13 @@ export default function ChatModal({
|
||||||
onClick={() => clearChat()}
|
onClick={() => clearChat()}
|
||||||
className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||||
>
|
>
|
||||||
<FaEraser className="w-4 h-4" />
|
<Eraser className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setModalOpen(false)}
|
onClick={() => setModalOpen(false)}
|
||||||
className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||||
>
|
>
|
||||||
<HiX className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -425,7 +383,7 @@ export default function ChatModal({
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<div className="bg-gray-100 dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
<div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||||
<span className="text-base text-gray-500">
|
<span className="text-base text-gray-500">
|
||||||
Start a conversation and click the agent’s thoughts{" "}
|
Start a conversation and click the agent’s thoughts{" "}
|
||||||
<span>
|
<span>
|
||||||
|
|
|
||||||
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