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
|
||||
.testenv/*
|
||||
langflow.db
|
||||
|
|
|
|||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -35,7 +35,7 @@
|
|||
"name": "Debug Frontend",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000/*",
|
||||
"url": "http://localhost:3000/",
|
||||
"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"
|
||||
typer = "^0.7.0"
|
||||
gunicorn = "^20.1.0"
|
||||
langchain = "^0.0.194"
|
||||
openai = "^0.27.7"
|
||||
langchain = "^0.0.200"
|
||||
openai = "^0.27.8"
|
||||
types-pyyaml = "^6.0.12.8"
|
||||
dill = "^0.3.6"
|
||||
pandas = "^1.5.3"
|
||||
chromadb = "^0.3.21"
|
||||
huggingface-hub = "^0.13.3"
|
||||
|
|
@ -57,9 +56,13 @@ jina = "3.15.2"
|
|||
sentence-transformers = "^2.2.2"
|
||||
ctransformers = "^0.2.2"
|
||||
cohere = "^4.6.0"
|
||||
sqlmodel = "^0.0.8"
|
||||
faiss-cpu = "^1.7.4"
|
||||
anthropic = "^0.2.9"
|
||||
orjson = "^3.9.0"
|
||||
multiprocess = "^0.70.14"
|
||||
cachetools = "^5.3.1"
|
||||
types-cachetools = "^5.3.0.5"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import sys
|
||||
import time
|
||||
from fastapi import FastAPI
|
||||
import httpx
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Optional
|
||||
import socket
|
||||
from rich.panel import Panel
|
||||
from rich import box
|
||||
from rich import print as rprint
|
||||
import typer
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from fastapi.responses import FileResponse
|
||||
from langflow.main import create_app
|
||||
from langflow.settings import settings
|
||||
from langflow.utils.logger import configure, logger
|
||||
|
|
@ -26,10 +28,19 @@ def get_number_of_workers(workers=None):
|
|||
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."""
|
||||
if config:
|
||||
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():
|
||||
|
|
@ -93,6 +104,10 @@ def serve(
|
|||
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"),
|
||||
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(
|
||||
None,
|
||||
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(
|
||||
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.
|
||||
|
||||
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:
|
||||
|
|
@ -125,19 +129,22 @@ def serve(
|
|||
load_dotenv(env_file)
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
update_settings(config, dev=dev)
|
||||
app = create_app()
|
||||
update_settings(
|
||||
config, dev=dev, database_url=database_url, remove_api_keys=remove_api_keys
|
||||
)
|
||||
# get the directory of the current file
|
||||
if not path:
|
||||
frontend_path = Path(__file__).parent
|
||||
static_files_dir = frontend_path / "frontend"
|
||||
else:
|
||||
static_files_dir = Path(path)
|
||||
app.mount(
|
||||
"/",
|
||||
StaticFiles(directory=static_files_dir, html=True),
|
||||
name="static",
|
||||
)
|
||||
|
||||
app = create_app()
|
||||
setup_static_files(app, static_files_dir)
|
||||
# check if port is being used
|
||||
if is_port_in_use(port, host):
|
||||
port = get_free_port(port)
|
||||
|
||||
options = {
|
||||
"bind": f"{host}:{port}",
|
||||
"workers": get_number_of_workers(workers),
|
||||
|
|
@ -152,7 +159,8 @@ def serve(
|
|||
status_code = 0
|
||||
while status_code != 200:
|
||||
try:
|
||||
status_code = httpx.get(f"http://{host}:{port}").status_code
|
||||
status_code = httpx.get(f"http://{host}:{port}/health").status_code
|
||||
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
|
||||
|
|
@ -161,11 +169,64 @@ def serve(
|
|||
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):
|
||||
# console = Console()
|
||||
|
||||
word = "LangFlow"
|
||||
colors = ["#690080", "#660099", "#4d00b3", "#3300cc", "#1a00e6", "#0000ff"]
|
||||
colors = ["#3300cc"]
|
||||
|
||||
styled_word = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
# Router for base api
|
||||
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(endpoints_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.validate import router as validate_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 (
|
||||
APIRouter,
|
||||
HTTPException,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
WebSocketException,
|
||||
status,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from langflow.api.v1.schemas import BuiltResponse, InitResponse
|
||||
|
||||
from langflow.chat.manager import ChatManager
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.utils.logger import logger
|
||||
from cachetools import LRUCache
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(tags=["Chat"])
|
||||
chat_manager = ChatManager()
|
||||
flow_data_store: LRUCache = LRUCache(maxsize=10)
|
||||
|
||||
|
||||
@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."""
|
||||
try:
|
||||
await chat_manager.handle_websocket(client_id, websocket)
|
||||
if client_id in chat_manager.in_memory_cache:
|
||||
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:
|
||||
logger.error(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)
|
||||
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 importlib.metadata import version
|
||||
from langflow.database.models.flow import Flow
|
||||
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 (
|
||||
ExportedFlow,
|
||||
GraphData,
|
||||
PredictRequest,
|
||||
PredictResponse,
|
||||
)
|
||||
|
||||
from langflow.interface.types import build_langchain_types_dict
|
||||
from langflow.database.base import get_session
|
||||
from sqlmodel import Session
|
||||
|
||||
# build router
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["Base"])
|
||||
|
||||
|
||||
@router.get("/all")
|
||||
|
|
@ -22,16 +22,34 @@ def get_all():
|
|||
return build_langchain_types_dict()
|
||||
|
||||
|
||||
@router.post("/predict", response_model=PredictResponse)
|
||||
async def get_load(predict_request: PredictRequest):
|
||||
try:
|
||||
from langflow.processing.process import process_graph_cached
|
||||
@router.post("/predict/{flow_id}", response_model=PredictResponse)
|
||||
async def predict_flow(
|
||||
predict_request: PredictRequest,
|
||||
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
|
||||
graph_data: GraphData = exported_flow.data
|
||||
data = graph_data.dict()
|
||||
response = process_graph_cached(data, predict_request.message)
|
||||
return PredictResponse(result=response.get("result", ""))
|
||||
try:
|
||||
flow = session.get(Flow, flow_id)
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
||||
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:
|
||||
# Log stack trace
|
||||
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 pydantic import BaseModel, validator
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from langflow.database.models.flow import FlowCreate, FlowRead
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
class GraphData(BaseModel):
|
||||
|
|
@ -23,13 +23,30 @@ class PredictRequest(BaseModel):
|
|||
"""Predict request schema."""
|
||||
|
||||
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):
|
||||
"""Predict response schema."""
|
||||
|
||||
result: str
|
||||
intermediate_steps: str = ""
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
|
|
@ -68,3 +85,19 @@ class FileResponse(ChatMessage):
|
|||
if v not in ["image", "csv"]:
|
||||
raise ValueError("data_type must be image or csv")
|
||||
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
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/validate", tags=["validate"])
|
||||
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||
|
||||
|
||||
@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",
|
||||
]
|
||||
|
|
|
|||
215
src/backend/langflow/cache/base.py
vendored
215
src/backend/langflow/cache/base.py
vendored
|
|
@ -1,154 +1,87 @@
|
|||
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
|
||||
|
||||
import dill # type: ignore
|
||||
|
||||
CACHE: Dict[str, Any] = {}
|
||||
import abc
|
||||
|
||||
|
||||
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:
|
||||
class BaseCache(abc.ABC):
|
||||
"""
|
||||
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.
|
||||
Abstract base class for a cache.
|
||||
"""
|
||||
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)
|
||||
@abc.abstractmethod
|
||||
def get(self, key):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
# Create the full file path
|
||||
file_path = os.path.join(cache_path, file_name)
|
||||
Args:
|
||||
key: The key of the item to retrieve.
|
||||
|
||||
# Save the binary content to the file
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(decoded_bytes)
|
||||
Returns:
|
||||
The value associated with the key, or None if the key is not found.
|
||||
"""
|
||||
|
||||
return file_path
|
||||
@abc.abstractmethod
|
||||
def set(self, key, value):
|
||||
"""
|
||||
Add an item to the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
|
||||
@create_cache_folder
|
||||
def save_cache(hash_val: str, chat_data, clean_old_cache_files: bool):
|
||||
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
||||
with cache_path.open("wb") as cache_file:
|
||||
dill.dump(chat_data, cache_file)
|
||||
@abc.abstractmethod
|
||||
def delete(self, key):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
if clean_old_cache_files:
|
||||
clear_old_cache_files()
|
||||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def clear(self):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
|
||||
@create_cache_folder
|
||||
def load_cache(hash_val):
|
||||
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
||||
if cache_path.exists():
|
||||
with cache_path.open("rb") as cache_file:
|
||||
return dill.load(cache_file)
|
||||
return None
|
||||
@abc.abstractmethod
|
||||
def __contains__(self, key):
|
||||
"""
|
||||
Check if the key is in the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to check.
|
||||
|
||||
Returns:
|
||||
True if the key is in the cache, False otherwise.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Retrieve an item from the cache using the square bracket notation.
|
||||
|
||||
Args:
|
||||
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):
|
||||
super().__init__()
|
||||
self.CACHE = {}
|
||||
self._cache = {}
|
||||
self.current_client_id = None
|
||||
self.current_cache = {}
|
||||
|
||||
|
|
@ -68,12 +68,12 @@ class CacheManager(Subject):
|
|||
"""
|
||||
previous_client_id = self.current_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:
|
||||
yield
|
||||
finally:
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
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 json
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langflow.cache.flow import InMemoryCache
|
||||
|
||||
|
||||
class ChatHistory(Subject):
|
||||
|
|
@ -46,6 +48,7 @@ class ChatManager:
|
|||
self.chat_history = ChatHistory()
|
||||
self.cache_manager = cache_manager
|
||||
self.cache_manager.attach(self.update)
|
||||
self.in_memory_cache = InMemoryCache()
|
||||
|
||||
def on_chat_history_update(self):
|
||||
"""Send the last chat message to the client."""
|
||||
|
|
@ -99,24 +102,30 @@ class ChatManager:
|
|||
websocket = self.active_connections[client_id]
|
||||
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
|
||||
chat_message = payload.pop("message", "")
|
||||
chat_message = ChatMessage(message=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="")
|
||||
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
|
||||
try:
|
||||
logger.debug("Generating result and thought")
|
||||
|
||||
result, intermediate_steps = await process_graph(
|
||||
graph_data=graph_data,
|
||||
is_first_message=is_first_message,
|
||||
langchain_object=langchain_object,
|
||||
chat_message=chat_message,
|
||||
websocket=self.active_connections[client_id],
|
||||
)
|
||||
|
|
@ -149,6 +158,14 @@ class ChatManager:
|
|||
await self.send_json(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):
|
||||
await self.connect(client_id, websocket)
|
||||
|
||||
|
|
@ -169,22 +186,24 @@ class ChatManager:
|
|||
continue
|
||||
|
||||
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:
|
||||
# Handle any exceptions that might occur
|
||||
logger.exception(e)
|
||||
# send a message to the client
|
||||
await self.active_connections[client_id].close(
|
||||
code=status.WS_1011_INTERNAL_ERROR, reason=str(e)[:120]
|
||||
logger.error(e)
|
||||
await self.close_connection(
|
||||
client_id=client_id,
|
||||
code=status.WS_1011_INTERNAL_ERROR,
|
||||
reason=str(e)[:120],
|
||||
)
|
||||
self.disconnect(client_id)
|
||||
finally:
|
||||
try:
|
||||
connection = self.active_connections.get(client_id)
|
||||
if connection:
|
||||
await connection.close(code=1000, reason="Client disconnected")
|
||||
self.disconnect(client_id)
|
||||
await self.close_connection(
|
||||
client_id=client_id,
|
||||
code=status.WS_1000_NORMAL_CLOSURE,
|
||||
reason="Client disconnected",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(e)
|
||||
self.disconnect(client_id)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,15 @@
|
|||
from fastapi import WebSocket
|
||||
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.interface.utils import try_setting_streaming_options
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
async def process_graph(
|
||||
graph_data: Dict,
|
||||
is_first_message: bool,
|
||||
langchain_object,
|
||||
chat_message: ChatMessage,
|
||||
websocket: WebSocket,
|
||||
):
|
||||
langchain_object = load_or_build_langchain_object(graph_data, is_first_message)
|
||||
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
||||
logger.debug("Loaded langchain object")
|
||||
|
||||
|
|
|
|||
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.graph.constants import VERTEX_TYPE_MAP
|
||||
|
|
@ -31,7 +31,7 @@ class Graph:
|
|||
Creates a graph from a payload.
|
||||
|
||||
Args:
|
||||
payload (Dict): The payload to create the graph from.
|
||||
payload (Dict): The payload to create the graph from.˜`
|
||||
|
||||
Returns:
|
||||
Graph: The created graph.
|
||||
|
|
@ -107,6 +107,47 @@ class Graph:
|
|||
raise ValueError("No root node found")
|
||||
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]:
|
||||
"""Returns the neighbors of a node."""
|
||||
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.interface import loading
|
||||
from langflow.interface.listing import ALL_TYPES_DICT
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class CSVAgent(CustomAgentExecutor):
|
|||
|
||||
|
||||
class VectorStoreAgent(CustomAgentExecutor):
|
||||
"""Vector Store agent"""
|
||||
"""Vector store agent"""
|
||||
|
||||
@staticmethod
|
||||
def function_name():
|
||||
|
|
@ -175,7 +175,7 @@ class SQLAgent(CustomAgentExecutor):
|
|||
def from_toolkit_and_llm(
|
||||
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)
|
||||
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
|
||||
|
||||
|
|
@ -252,7 +252,11 @@ class VectorStoreRouterAgent(CustomAgentExecutor):
|
|||
):
|
||||
"""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)
|
||||
llm_chain = LLMChain(
|
||||
llm=llm,
|
||||
|
|
@ -302,7 +306,7 @@ class InitializeAgent(CustomAgentExecutor):
|
|||
CUSTOM_AGENTS = {
|
||||
"JsonAgent": JsonAgent,
|
||||
"CSVAgent": CSVAgent,
|
||||
"initialize_agent": InitializeAgent,
|
||||
"AgentInitializer": InitializeAgent,
|
||||
"VectorStoreAgent": VectorStoreAgent,
|
||||
"VectorStoreRouterAgent": VectorStoreRouterAgent,
|
||||
"SQLAgent": SQLAgent,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ class ChainCreator(LangChainTypeCreator):
|
|||
return ChainFrontendNode
|
||||
|
||||
#! 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
|
||||
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.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)
|
||||
def build_langchain_object_with_caching(data_graph):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ from fastapi import FastAPI
|
|||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from langflow.api import router
|
||||
from langflow.database.base import create_db_and_tables
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create the FastAPI app and include the router."""
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
|
|
@ -25,6 +27,7 @@ def create_app():
|
|||
)
|
||||
|
||||
app.include_router(router)
|
||||
app.on_event("startup")(create_db_and_tables)
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -170,3 +170,28 @@ def load_flow_from_json(path: str, build=True):
|
|||
fix_memory_inputs(langchain_object)
|
||||
return langchain_object
|
||||
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] = []
|
||||
utilities: List[str] = []
|
||||
dev: bool = False
|
||||
database_url: str = "sqlite:///./langflow.db"
|
||||
remove_api_keys: bool = False
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
extra = "ignore"
|
||||
env_prefix = "LANGFLOW_"
|
||||
|
||||
@root_validator(allow_reuse=True)
|
||||
def validate_lists(cls, values):
|
||||
|
|
@ -46,6 +49,11 @@ class Settings(BaseSettings):
|
|||
self.utilities = new_settings.utilities or []
|
||||
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):
|
||||
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"]
|
||||
|
||||
def to_dict(self):
|
||||
|
|
@ -155,6 +155,7 @@ class CSVAgentNode(FrontendNode):
|
|||
|
||||
class InitializeAgentNode(FrontendNode):
|
||||
name: str = "AgentInitializer"
|
||||
display_name: str = "AgentInitializer"
|
||||
template: Template = Template(
|
||||
type_name="initialize_agent",
|
||||
fields=[
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ class ChainFrontendNode(FrontendNode):
|
|||
field.show = 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
|
||||
if field.name == "prompt" and field.value is None:
|
||||
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] = [
|
||||
"LLMChain",
|
||||
"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"]
|
||||
|
||||
def to_dict(self):
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@ from langflow.template.field.base import TemplateField
|
|||
from langflow.template.frontend_node.base import FrontendNode
|
||||
|
||||
|
||||
class DocumentLoaderFrontNode(FrontendNode):
|
||||
@staticmethod
|
||||
def build_template(
|
||||
suffixes: list, fileTypes: list, name: str = "file_path"
|
||||
) -> TemplateField:
|
||||
"""Build a template field for a document loader."""
|
||||
return TemplateField(
|
||||
field_type="file",
|
||||
required=True,
|
||||
show=True,
|
||||
name=name,
|
||||
value="",
|
||||
suffixes=suffixes,
|
||||
fileTypes=fileTypes,
|
||||
)
|
||||
def build_template(
|
||||
suffixes: list, fileTypes: list, name: str = "file_path"
|
||||
) -> TemplateField:
|
||||
"""Build a template field for a document loader."""
|
||||
return TemplateField(
|
||||
field_type="file",
|
||||
required=True,
|
||||
show=True,
|
||||
name=name,
|
||||
value="",
|
||||
suffixes=suffixes,
|
||||
fileTypes=fileTypes,
|
||||
)
|
||||
|
||||
|
||||
class DocumentLoaderFrontNode(FrontendNode):
|
||||
file_path_templates = {
|
||||
"AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
|
||||
"CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ class LLMFrontendNode(FrontendNode):
|
|||
field.name.title().replace("Openai", "OpenAI").replace("_", " ")
|
||||
).replace("Api", "API")
|
||||
|
||||
if "key" not in field.name.lower() and "token" not in field.name.lower():
|
||||
field.password = False
|
||||
|
||||
@staticmethod
|
||||
def format_azure_field(field: TemplateField):
|
||||
if field.name == "model_name":
|
||||
|
|
@ -22,7 +25,6 @@ class LLMFrontendNode(FrontendNode):
|
|||
field.value = "azure"
|
||||
elif field.name == "openai_api_version":
|
||||
field.password = False
|
||||
field.value = "2023-03-15-preview"
|
||||
|
||||
@staticmethod
|
||||
def format_llama_field(field: TemplateField):
|
||||
|
|
@ -44,7 +46,10 @@ class LLMFrontendNode(FrontendNode):
|
|||
if field.name in SHOW_FIELDS:
|
||||
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.show = True
|
||||
# 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"]
|
||||
|
||||
def to_dict(self):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,16 @@ OPENAI_MODELS = [
|
|||
"text-babbage-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 = [
|
||||
"claude-v1", # largest model, ideal for a wide range of more complex tasks.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import ast
|
||||
import contextlib
|
||||
import importlib
|
||||
import types
|
||||
from typing import Dict
|
||||
|
|
@ -147,11 +148,8 @@ def create_function(code, function_name):
|
|||
code_obj = compile(
|
||||
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
|
||||
)
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
exec(code_obj, exec_globals, locals())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
exec_globals[function_name] = locals()[function_name]
|
||||
|
||||
# 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
|
||||
19350
src/frontend/package-lock.json
generated
19350
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,101 +1,106 @@
|
|||
{
|
||||
"name": "langflow",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@headlessui/react": "^1.7.10",
|
||||
"@heroicons/react": "^2.0.15",
|
||||
"@mui/material": "^5.11.9",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tabler/icons-react": "^2.18.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"ace-builds": "^1.16.0",
|
||||
"add": "^2.0.6",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.3.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tabs": "^6.0.0",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.5.5",
|
||||
"rehype-mathjax": "^4.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"shadcn-ui": "^0.1.3",
|
||||
"switch": "^0.0.0",
|
||||
"table": "^6.8.1",
|
||||
"tailwind-merge": "^1.13.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:docker": "vite --host 0.0.0.0",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://127.0.0.1:7860",
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.5"
|
||||
}
|
||||
"name": "langflow",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@headlessui/react": "^1.7.10",
|
||||
"@heroicons/react": "^2.0.15",
|
||||
"@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-dialog": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tabler/icons-react": "^2.18.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"ace-builds": "^1.16.0",
|
||||
"add": "^2.0.6",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.3.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tabs": "^6.0.0",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.5.5",
|
||||
"rehype-mathjax": "^4.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"shadcn-ui": "^0.1.3",
|
||||
"short-unique-id": "^4.4.4",
|
||||
"switch": "^0.0.0",
|
||||
"table": "^6.8.1",
|
||||
"tailwind-merge": "^1.13.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:docker": "vite --host 0.0.0.0",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://127.0.0.1:7860",
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ module.exports = {
|
|||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import _ from "lodash";
|
|||
import ErrorAlert from "./alerts/error";
|
||||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
import ExtraSidebar from "./components/ExtraSidebarComponent";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||
import { TabsContext } from "./contexts/tabsContext";
|
||||
import { getVersion } from "./controllers/API";
|
||||
import Router from "./routes";
|
||||
import Header from "./components/headerComponent";
|
||||
|
||||
export default function App() {
|
||||
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
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
|
|
@ -111,7 +104,6 @@ export default function App() {
|
|||
return (
|
||||
//need parent component with width and height
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex grow-0 shrink basis-auto"></div>
|
||||
<ErrorBoundary
|
||||
onReset={() => {
|
||||
window.localStorage.removeItem("tabsData");
|
||||
|
|
@ -121,16 +113,8 @@ export default function App() {
|
|||
}}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
||||
<ExtraSidebar />
|
||||
{/* 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>
|
||||
<Header />
|
||||
<Router />
|
||||
</ErrorBoundary>
|
||||
<div></div>
|
||||
<div
|
||||
|
|
@ -166,14 +150,6 @@ export default function App() {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||
import Tooltip from "../../../../components/TooltipComponent";
|
||||
import {
|
||||
classNames,
|
||||
groupByFamily,
|
||||
isValidConnection,
|
||||
toFirstUpperCase,
|
||||
} from "../../../../utils";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import InputComponent from "../../../../components/inputComponent";
|
||||
import ToggleComponent from "../../../../components/toggleComponent";
|
||||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
|
|
@ -24,6 +21,8 @@ import { nodeNames, nodeIcons } from "../../../../utils";
|
|||
import React from "react";
|
||||
import { nodeColors } from "../../../../utils";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
|
||||
export default function ParameterComponent({
|
||||
left,
|
||||
|
|
@ -40,6 +39,9 @@ export default function ParameterComponent({
|
|||
const refHtml = useRef(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const [position, setPosition] = useState(0);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const { setTabsState, tabId } = useContext(TabsContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
||||
|
|
@ -54,12 +56,27 @@ export default function ParameterComponent({
|
|||
const [enabled, setEnabled] = useState(
|
||||
data.node.template[name]?.value ?? false
|
||||
);
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||
const { save } = useContext(TabsContext);
|
||||
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(() => {
|
||||
const groupedObj = groupByFamily(myData, tooltipTitle);
|
||||
|
||||
|
|
@ -104,7 +121,7 @@ export default function ParameterComponent({
|
|||
return (
|
||||
<div
|
||||
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")}>
|
||||
|
|
@ -125,6 +142,7 @@ export default function ParameterComponent({
|
|||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
open={refHtml?.current?.length > 0}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
|
|
@ -158,19 +176,13 @@ export default function ParameterComponent({
|
|||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
|
|
@ -178,84 +190,72 @@ export default function ParameterComponent({
|
|||
disableCopyPaste={true}
|
||||
password={data.node.template[name].password ?? false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2">
|
||||
<ToggleComponent
|
||||
<ToggleShadComponent
|
||||
disabled={disabled}
|
||||
enabled={enabled}
|
||||
setEnabled={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
handleOnNewValue(t);
|
||||
setEnabled(t);
|
||||
save();
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "float" ? (
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 w-full">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true &&
|
||||
type === "str" &&
|
||||
data.node.template[name].options ? (
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={(newValue) => (data.node.template[name].value = newValue)}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
<div className="w-full">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : left === true && type === "code" ? (
|
||||
<CodeAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : left === true && type === "file" ? (
|
||||
<InputFileComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
fileTypes={data.node.template[name].fileTypes}
|
||||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[name].content = t;
|
||||
save();
|
||||
}}
|
||||
></InputFileComponent>
|
||||
) : left === true && type === "int" ? (
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 w-full">
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
<PromptAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,18 @@
|
|||
import {
|
||||
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 { classNames, nodeColors, nodeIcons, toTitleCase } from "../../utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
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 { alertContext } from "../../contexts/alertContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import NodeModal from "../../modals/NodeModal";
|
||||
import { useCallback } from "react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { debounce } from "../../utils";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import { postValidateNode } from "../../controllers/API";
|
||||
import { useSSE } from "../../contexts/SSEContext";
|
||||
|
||||
export default function GenericNode({
|
||||
data,
|
||||
selected,
|
||||
|
|
@ -44,46 +23,30 @@ export default function GenericNode({
|
|||
const { setErrorData } = useContext(alertContext);
|
||||
const showError = useRef(true);
|
||||
const { types, deleteNode } = useContext(typesContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
|
||||
const { closePopUp, openPopUp } = useContext(PopUpContext);
|
||||
|
||||
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
|
||||
const [validationStatus, setValidationStatus] = useState(null);
|
||||
// State for outline color
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const { save } = useContext(TabsContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const [params, setParams] = useState([]);
|
||||
const { sseData, isBuilding } = useSSE();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance) {
|
||||
// setParams(Object.values(reactFlowInstance.toObject()));
|
||||
// }
|
||||
// }, [save]);
|
||||
|
||||
// New useEffect to watch for changes in sseData and update validation status
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance) {
|
||||
setParams(Object.values(reactFlowInstance.toObject()));
|
||||
const relevantData = sseData[data.id];
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [save]);
|
||||
|
||||
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]);
|
||||
}, [sseData, data.id]);
|
||||
|
||||
if (!Icon) {
|
||||
if (showError.current) {
|
||||
|
|
@ -98,6 +61,8 @@ export default function GenericNode({
|
|||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeToolbar>
|
||||
|
|
@ -110,11 +75,11 @@ export default function GenericNode({
|
|||
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
<Icon
|
||||
className="h-10 w-10 rounded p-1"
|
||||
|
|
@ -124,7 +89,7 @@ export default function GenericNode({
|
|||
/>
|
||||
<div className="ml-2 truncate">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -172,7 +137,7 @@ export default function GenericNode({
|
|||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
!validationStatus
|
||||
!validationStatus || isBuilding
|
||||
? "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",
|
||||
"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 className="h-full w-full py-5">
|
||||
<div className="w-full px-5 pb-3 text-sm text-gray-500 dark:text-gray-300">
|
||||
<div className="h-full w-full py-5 text-gray-800">
|
||||
<div className="w-full px-5 pb-3 text-sm text-muted-foreground">
|
||||
{data.node.description}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export default function SingleAlert({
|
|||
{dropItem.link ? (
|
||||
<Link
|
||||
to={dropItem.link}
|
||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-blue-600"
|
||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-ring"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default function NoticeAlert({
|
|||
{link !== "" ? (
|
||||
<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
|
||||
</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 "
|
||||
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
|
||||
>
|
||||
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start">
|
||||
<div className="flex px-4 justify-between align-middle w-full">
|
||||
<span className="text-gray-900 dark:text-white py-[2px] font-medium "></span>
|
||||
</div>
|
||||
<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 flex-grow flex-col w-full">
|
||||
{extraNavigation.options ? (
|
||||
<div className="p-4">
|
||||
|
|
@ -36,8 +33,8 @@ export default function ExtraSidebar() {
|
|||
to={item.href}
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
? "bg-muted 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"
|
||||
)}
|
||||
>
|
||||
|
|
@ -63,9 +60,9 @@ export default function ExtraSidebar() {
|
|||
<Disclosure.Button
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
? "bg-muted 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-1 focus:ring-indigo-500"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
|
|
@ -96,8 +93,8 @@ export default function ExtraSidebar() {
|
|||
to={subItem.href}
|
||||
className={classNames(
|
||||
subItem.href.split("/")[3] === current[5]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
? "bg-muted 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"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const ShadTooltip = (props) => {
|
|||
<TooltipProvider>
|
||||
<Tooltip delayDuration={props.delayDuration}>
|
||||
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
side={props.side}
|
||||
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 {
|
||||
Bars3CenterLeftIcon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { nodeColors } from "../../../utils";
|
||||
import { PopUpContext } from "../../../contexts/popUpContext";
|
||||
import { useContext } from "react";
|
||||
import ChatModal from "../../../modals/chatModal";
|
||||
import { MessagesSquare } from "lucide-react";
|
||||
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function ChatTrigger({ open, setOpen, isBuilt }) {
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
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 (
|
||||
<Transition
|
||||
show={!open}
|
||||
|
|
@ -23,16 +31,16 @@ export default function ChatTrigger({ open, setOpen }) {
|
|||
>
|
||||
<div className="absolute bottom-4 right-3">
|
||||
<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"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
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={handleClick}
|
||||
>
|
||||
<button>
|
||||
<div className="flex gap-3 items-center">
|
||||
<ChatBubbleBottomCenterTextIcon
|
||||
className="h-6 w-6 mt-1"
|
||||
<div className="flex gap-3">
|
||||
<MessagesSquare
|
||||
className="pth-6 w-6 fill-[#5c8be1] stroke-1 stroke-[#5c8be1]"
|
||||
style={{ color: "white" }}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { ChatMessageType, ChatType } from "../../types/chat";
|
||||
import { useNodes } from "reactflow";
|
||||
import { ChatType } from "../../types/chat";
|
||||
import ChatTrigger from "./chatTrigger";
|
||||
import BuildTrigger from "./buildTrigger";
|
||||
import ChatModal from "../../modals/chatModal";
|
||||
|
||||
import _ from "lodash";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import { NodeType } from "../../types/flow";
|
||||
|
||||
export default function Chat({ flow }: ChatType) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isBuilt, setIsBuilt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.key === "K" || event.key === "k") &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
isBuilt
|
||||
) {
|
||||
event.preventDefault();
|
||||
setOpen((oldState) => !oldState);
|
||||
|
|
@ -22,11 +27,58 @@ export default function Chat({ flow }: ChatType) {
|
|||
return () => {
|
||||
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 (
|
||||
<>
|
||||
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||
<ChatTrigger open={open} setOpen={setOpen} />
|
||||
{isBuilt ? (
|
||||
<div>
|
||||
<BuildTrigger
|
||||
open={open}
|
||||
flow={flow}
|
||||
setIsBuilt={setIsBuilt}
|
||||
isBuilt={isBuilt}
|
||||
/>
|
||||
<ChatModal key={flow.id} flow={flow} 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 TextAreaModal from "../../modals/textAreaModal";
|
||||
import { TextAreaComponentType } from "../../types/components";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function CodeAreaComponent({
|
||||
value,
|
||||
|
|
@ -19,13 +20,18 @@ export default function CodeAreaComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
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
|
||||
onClick={() => {
|
||||
openPopUp(
|
||||
|
|
@ -40,12 +46,14 @@ export default function CodeAreaComponent({
|
|||
}}
|
||||
className={
|
||||
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 block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
? "truncate 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" +
|
||||
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" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "Text empty"}
|
||||
{myValue !== "" ? myValue : "Type something..."}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -61,7 +69,7 @@ export default function CodeAreaComponent({
|
|||
}}
|
||||
>
|
||||
{!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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { ChevronUpDownIcon, CheckIcon } from "@heroicons/react/24/outline";
|
|||
import { Fragment, useState } from "react";
|
||||
import { DropDownComponentType } from "../../types/components";
|
||||
import { classNames } from "../../utils";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function Dropdown({
|
||||
value,
|
||||
options,
|
||||
onSelect,
|
||||
editNode = false,
|
||||
numberOfOptions = 0,
|
||||
}: DropDownComponentType) {
|
||||
let [internalValue, setInternalValue] = useState(
|
||||
value === "" || !value ? "Choose an option" : value
|
||||
|
|
@ -25,24 +27,20 @@ export default function Dropdown({
|
|||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
editNode ? "relative mt-0 w-full" : "relative mt-1 w-full"
|
||||
}
|
||||
>
|
||||
<div className={editNode ? "mt-1" : "relative mt-1"}>
|
||||
<Listbox.Button
|
||||
className={
|
||||
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 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"
|
||||
? "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" +
|
||||
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={
|
||||
editNode
|
||||
? "hidden"
|
||||
: "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
}
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
|
|
@ -62,8 +60,8 @@ export default function Dropdown({
|
|||
<Listbox.Options
|
||||
className={
|
||||
editNode
|
||||
? "arrow-hide"
|
||||
: "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"
|
||||
? "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]"
|
||||
: "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) => (
|
||||
|
|
@ -71,11 +69,8 @@ export default function Dropdown({
|
|||
key={id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active && !editNode
|
||||
? "text-white bg-indigo-600 dark:bg-indigo-500"
|
||||
: "text-gray-900",
|
||||
active && editNode
|
||||
? "text-white bg-gray-400 dark:bg-gray-500"
|
||||
active
|
||||
? " bg-accent dark:bg-white dark:text-gray-500"
|
||||
: "",
|
||||
editNode
|
||||
? "relative cursor-default select-none py-0.5 pl-3 pr-12 dark:text-gray-300 dark:bg-gray-800"
|
||||
|
|
@ -89,7 +84,7 @@ export default function Dropdown({
|
|||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
"block truncate "
|
||||
)}
|
||||
>
|
||||
{option}
|
||||
|
|
@ -98,13 +93,16 @@ export default function Dropdown({
|
|||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
editNode ? "text-gray-600" : "text-indigo-600",
|
||||
active ? "text-white" : "",
|
||||
active ? "text-white dark:text-black" : "",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { FloatComponentType } from "../../types/components";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function FloatComponent({
|
||||
value,
|
||||
|
|
@ -12,12 +13,21 @@ export default function FloatComponent({
|
|||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
|
||||
const step = 0.1;
|
||||
const min = 0;
|
||||
const max = 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -32,11 +42,24 @@ export default function FloatComponent({
|
|||
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||
}}
|
||||
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}
|
||||
className={
|
||||
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"
|
||||
: "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" +
|
||||
? "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" +
|
||||
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" : "")
|
||||
}
|
||||
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 { classNames } from "../../utils";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function InputComponent({
|
||||
value,
|
||||
|
|
@ -14,6 +16,8 @@ export default function InputComponent({
|
|||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
|
|
@ -21,6 +25,10 @@ export default function InputComponent({
|
|||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value ?? "");
|
||||
}, [closePopUp]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -38,12 +46,13 @@ export default function InputComponent({
|
|||
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||
}}
|
||||
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" : "",
|
||||
password && !pwdVisible && myValue !== "" ? "password" : "",
|
||||
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"
|
||||
: "focus:border-indigo-500 focus:ring-indigo-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 text-center" +
|
||||
INPUT_STYLE
|
||||
: "ring-offset-gray-200" + INPUT_STYLE,
|
||||
password && editNode ? "pr-8" : "pr-3"
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : "Type something..."}
|
||||
|
|
@ -71,7 +80,11 @@ export default function InputComponent({
|
|||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
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
|
||||
strokeLinecap="round"
|
||||
|
|
@ -86,7 +99,11 @@ export default function InputComponent({
|
|||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
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
|
||||
strokeLinecap="round"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FileComponentType } from "../../types/components";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function InputFileComponent({
|
||||
value,
|
||||
|
|
@ -37,6 +38,10 @@ export default function InputFileComponent({
|
|||
return false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
|
|
@ -73,8 +78,10 @@ export default function InputFileComponent({
|
|||
onClick={handleButtonClick}
|
||||
className={
|
||||
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 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" +
|
||||
? "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" +
|
||||
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" : "")
|
||||
}
|
||||
>
|
||||
|
|
@ -82,7 +89,7 @@ export default function InputFileComponent({
|
|||
</span>
|
||||
<button onClick={handleButtonClick}>
|
||||
{!editNode && (
|
||||
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-blue-600" />
|
||||
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-ring" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import { InputListComponentType } from "../../types/components";
|
|||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
|
||||
import _ from "lodash";
|
||||
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
export default function InputListComponent({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: InputListComponentType) {
|
||||
const [inputList, setInputList] = useState(value ?? [""]);
|
||||
useEffect(() => {
|
||||
|
|
@ -21,7 +22,7 @@ export default function InputListComponent({
|
|||
<div
|
||||
className={
|
||||
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
|
||||
"flex flex-col gap-3"
|
||||
"flex flex-col gap-3 py-2"
|
||||
}
|
||||
>
|
||||
{inputList.map((i, idx) => (
|
||||
|
|
@ -30,8 +31,12 @@ export default function InputListComponent({
|
|||
type="text"
|
||||
value={i}
|
||||
className={
|
||||
"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
editNode
|
||||
? "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..."
|
||||
onChange={(e) => {
|
||||
|
|
@ -54,7 +59,7 @@ export default function InputListComponent({
|
|||
onChange(inputList);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 hover:text-blue-600" />
|
||||
<PlusIcon className={"w-4 h-4 hover:text-ring"} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from "react";
|
|||
import { FloatComponentType } from "../../types/components";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { classNames } from "../../utils";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function IntComponent({
|
||||
value,
|
||||
|
|
@ -12,6 +13,7 @@ export default function IntComponent({
|
|||
}: FloatComponentType) {
|
||||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
const min = 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -19,6 +21,11 @@ export default function IntComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -53,11 +60,20 @@ export default function IntComponent({
|
|||
}
|
||||
}}
|
||||
type="number"
|
||||
step="1"
|
||||
min={min}
|
||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value < min.toString()) {
|
||||
e.target.value = min.toString();
|
||||
}
|
||||
}}
|
||||
value={myValue}
|
||||
className={
|
||||
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"
|
||||
: "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" +
|
||||
? "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" +
|
||||
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" : "")
|
||||
}
|
||||
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">
|
||||
<svg
|
||||
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"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import CodeAreaModal from "../../modals/codeAreaModal";
|
||||
import TextAreaModal from "../../modals/textAreaModal";
|
||||
import { TextAreaComponentType } from "../../types/components";
|
||||
import GenericModal from "../../modals/genericModal";
|
||||
import { TypeModal } from "../../utils";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function PromptAreaComponent({
|
||||
value,
|
||||
|
|
@ -21,6 +20,11 @@ export default function PromptAreaComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -45,12 +49,13 @@ export default function PromptAreaComponent({
|
|||
}}
|
||||
className={
|
||||
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 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" +
|
||||
? "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" +
|
||||
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" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "-"}
|
||||
{myValue !== "" ? myValue : "Type your prompt here"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -69,7 +74,7 @@ export default function PromptAreaComponent({
|
|||
}}
|
||||
>
|
||||
{!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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { PopUpContext } from "../../contexts/popUpContext";
|
|||
import { TextAreaComponentType } from "../../types/components";
|
||||
import GenericModal from "../../modals/genericModal";
|
||||
import { TypeModal } from "../../utils";
|
||||
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
export default function TextAreaComponent({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -12,16 +12,28 @@ export default function TextAreaComponent({
|
|||
editNode = false,
|
||||
}: TextAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const { openPopUp, closePopUp } = useContext(PopUpContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [closePopUp]);
|
||||
|
||||
return (
|
||||
<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
|
||||
onClick={() => {
|
||||
openPopUp(
|
||||
|
|
@ -39,12 +51,13 @@ export default function TextAreaComponent({
|
|||
}}
|
||||
className={
|
||||
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 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" +
|
||||
? "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" +
|
||||
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" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "Text empty"}
|
||||
{myValue !== "" ? myValue : "Type something..."}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -63,7 +76,7 @@ export default function TextAreaComponent({
|
|||
}}
|
||||
>
|
||||
{!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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ export default function ToggleComponent({
|
|||
setEnabled(x);
|
||||
}}
|
||||
className={classNames(
|
||||
enabled ? "bg-indigo-600" : "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"
|
||||
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-1 focus:ring-primary focus:ring-offset-1"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { classNames } from "../../utils";
|
||||
import { useEffect } from "react";
|
||||
import { ToggleComponentType } from "../../types/components";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
|
@ -7,18 +6,38 @@ export default function ToggleShadComponent({
|
|||
enabled,
|
||||
setEnabled,
|
||||
disabled,
|
||||
size,
|
||||
}: ToggleComponentType) {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}, [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 (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<Switch
|
||||
style={{
|
||||
transform: "scaleX(0.6) scaleY(0.6)",
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
className="data-[state=unchecked]:bg-slate-500"
|
||||
checked={enabled}
|
||||
onCheckedChange={(x: boolean) => {
|
||||
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",
|
||||
outline:
|
||||
"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:
|
||||
"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",
|
||||
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
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...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
|
||||
|
||||
import { FlowType } from "./types/flow";
|
||||
import { buildTweaks } from "./utils";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Export Dialog (Toolbar)
|
||||
* @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)
|
||||
|
|
@ -18,22 +27,382 @@ export const CODE_DIALOG_SUBTITLE =
|
|||
* @constant
|
||||
*/
|
||||
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
|
||||
* @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
|
||||
* @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
|
||||
* @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> }) {
|
||||
setErrorDataState(newState);
|
||||
setErrorOpen(true);
|
||||
if (newState.title) {
|
||||
if (newState.title && newState.title !== "") {
|
||||
setNotificationCenter(true);
|
||||
pushNotificationList({
|
||||
type: "error",
|
||||
|
|
@ -97,7 +97,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
function setNoticeData(newState: { title: string; link?: string }) {
|
||||
setNoticeDataState(newState);
|
||||
setNoticeOpen(true);
|
||||
if (newState.title) {
|
||||
if (newState.title && newState.title !== "") {
|
||||
// Add new notice to notification center
|
||||
setNotificationCenter(true);
|
||||
pushNotificationList({
|
||||
|
|
@ -117,7 +117,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
setSuccessOpen(true); // open the success alert
|
||||
|
||||
// If the new state has a "title" property, add a new success notification to the list
|
||||
if (newState.title) {
|
||||
if (newState.title && newState.title !== "") {
|
||||
setNotificationCenter(true); // show the notification center
|
||||
pushNotificationList({
|
||||
// add the new notification to the list
|
||||
|
|
|
|||
|
|
@ -5,22 +5,31 @@ import { LocationProvider } from "./locationContext";
|
|||
import PopUpProvider from "./popUpContext";
|
||||
import { TabsProvider } from "./tabsContext";
|
||||
import { TypesProvider } from "./typesContext";
|
||||
import { ReactFlowProvider } from "reactflow";
|
||||
import { UndoRedoProvider } from "./undoRedoContext";
|
||||
import { SSEProvider } from "./SSEContext";
|
||||
|
||||
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
||||
//element to wrap all context
|
||||
return (
|
||||
<>
|
||||
<DarkProvider>
|
||||
<TypesProvider>
|
||||
<LocationProvider>
|
||||
<AlertProvider>
|
||||
<TabsProvider>
|
||||
<PopUpProvider>{children}</PopUpProvider>
|
||||
</TabsProvider>
|
||||
</AlertProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
</DarkProvider>
|
||||
<ReactFlowProvider>
|
||||
<DarkProvider>
|
||||
<TypesProvider>
|
||||
<LocationProvider>
|
||||
<AlertProvider>
|
||||
<SSEProvider>
|
||||
<TabsProvider>
|
||||
<UndoRedoProvider>
|
||||
<PopUpProvider>{children}</PopUpProvider>
|
||||
</UndoRedoProvider>
|
||||
</TabsProvider>
|
||||
</SSEProvider>
|
||||
</AlertProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
</DarkProvider>
|
||||
</ReactFlowProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,38 +7,52 @@ import {
|
|||
useContext,
|
||||
} from "react";
|
||||
import { FlowType, NodeType } from "../types/flow";
|
||||
import { LangFlowState, TabsContextType } from "../types/tabs";
|
||||
import { TabsContextType, TabsState } from "../types/tabs";
|
||||
import {
|
||||
normalCaseToSnakeCase,
|
||||
updateIds,
|
||||
updateObject,
|
||||
updateTemplate,
|
||||
getRandomDescription,
|
||||
getRandomName,
|
||||
} from "../utils";
|
||||
import { alertContext } from "./alertContext";
|
||||
import { typesContext } from "./typesContext";
|
||||
import { APITemplateType, TemplateVariableType } from "../types/api";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { APITemplateType } from "../types/api";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { addEdge } from "reactflow";
|
||||
import {
|
||||
readFlowsFromDatabase,
|
||||
deleteFlowFromDatabase,
|
||||
saveFlowToDatabase,
|
||||
downloadFlowsFromDatabase,
|
||||
uploadFlowsToDatabase,
|
||||
updateFlowInDatabase,
|
||||
} from "../controllers/API";
|
||||
import _ from "lodash";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
const TabsContextInitialValue: TabsContextType = {
|
||||
save: () => {},
|
||||
tabIndex: 0,
|
||||
setTabIndex: (index: number) => {},
|
||||
tabId: "",
|
||||
setTabId: (index: string) => {},
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: (flowData?: any) => {},
|
||||
addFlow: async (flowData?: any) => "",
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => uuidv4(),
|
||||
incrementNodeId: () => uid(),
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
downloadFlows: () => {},
|
||||
uploadFlows: () => {},
|
||||
uploadFlow: () => {},
|
||||
hardReset: () => {},
|
||||
saveFlow: async (flow: FlowType) => {},
|
||||
disableCopyPaste: false,
|
||||
setDisableCopyPaste: (state: boolean) => {},
|
||||
lastCopiedSelection: null,
|
||||
setLastCopiedSelection: (selection: any) => {},
|
||||
|
||||
getNodeId: () => "",
|
||||
tabsState: {},
|
||||
setTabsState: (state: TabsState) => {},
|
||||
getNodeId: (nodeType: string) => "",
|
||||
paste: (
|
||||
selection: { nodes: any; edges: any },
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
|
|
@ -51,17 +65,21 @@ export const TabsContext = createContext<TabsContextType>(
|
|||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
const { setErrorData, setNoticeData } = useContext(alertContext);
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const [tabId, setTabId] = useState("");
|
||||
|
||||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||
const [id, setId] = useState(uuidv4());
|
||||
const [id, setId] = useState(uid());
|
||||
const { templates, reactFlowInstance } = useContext(typesContext);
|
||||
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
|
||||
const [tabsState, setTabsState] = useState<TabsState>({});
|
||||
|
||||
const newNodeId = useRef(uuidv4());
|
||||
const newNodeId = useRef(uid());
|
||||
function incrementNodeId() {
|
||||
newNodeId.current = uuidv4();
|
||||
newNodeId.current = uid();
|
||||
return newNodeId.current;
|
||||
}
|
||||
|
||||
function save() {
|
||||
// added clone deep to avoid mutating the original object
|
||||
let Saveflows = _.cloneDeep(flows);
|
||||
|
|
@ -83,71 +101,156 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
window.localStorage.setItem(
|
||||
"tabsData",
|
||||
JSON.stringify({ tabIndex, flows: Saveflows, id })
|
||||
JSON.stringify({ tabId, flows: Saveflows, id })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
//get tabs locally saved
|
||||
let cookie = window.localStorage.getItem("tabsData");
|
||||
if (cookie && Object.keys(templates).length > 0) {
|
||||
let cookieObject: LangFlowState = JSON.parse(cookie);
|
||||
try {
|
||||
cookieObject.flows.forEach((flow) => {
|
||||
if (!flow.data) {
|
||||
return;
|
||||
}
|
||||
flow.data.edges.forEach((edge) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555555" };
|
||||
});
|
||||
// function loadCookie(cookie: string) {
|
||||
// if (cookie && Object.keys(templates).length > 0) {
|
||||
// let cookieObject: LangFlowState = JSON.parse(cookie);
|
||||
// try {
|
||||
// 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);
|
||||
// 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]);
|
||||
|
||||
useEffect(() => {
|
||||
//save tabs locally
|
||||
// console.log(id);
|
||||
save();
|
||||
}, [flows, id, tabIndex, newNodeId]);
|
||||
function getTabsDataFromDB() {
|
||||
//get tabs from db
|
||||
return readFlowsFromDatabase();
|
||||
}
|
||||
function processDBData(DbData) {
|
||||
DbData.forEach((flow) => {
|
||||
try {
|
||||
if (!flow.data) {
|
||||
return;
|
||||
}
|
||||
processFlowEdges(flow);
|
||||
processFlowNodes(flow);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processFlowEdges(flow) {
|
||||
flow.data.edges.forEach((edge) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555555" };
|
||||
});
|
||||
}
|
||||
|
||||
function processFlowNodes(flow) {
|
||||
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) {
|
||||
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"];
|
||||
}
|
||||
|
||||
function updateNodeEdges(flow, node, template) {
|
||||
flow.data.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateNodeDescription(node, template) {
|
||||
node.data.node.description = template["description"];
|
||||
}
|
||||
|
||||
function updateNodeTemplate(node, template) {
|
||||
node.data.node.template = updateTemplate(
|
||||
template["template"] as unknown as APITemplateType,
|
||||
node.data.node.template as APITemplateType
|
||||
);
|
||||
}
|
||||
|
||||
function updateStateWithDbData(tabsData) {
|
||||
setFlows(tabsData);
|
||||
}
|
||||
|
||||
function hardReset() {
|
||||
newNodeId.current = uuidv4();
|
||||
setTabIndex(0);
|
||||
newNodeId.current = uid();
|
||||
setTabId("");
|
||||
|
||||
setFlows([]);
|
||||
setId(uuidv4());
|
||||
setId(uid());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,7 +265,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
// create a link element and set its properties
|
||||
const link = document.createElement("a");
|
||||
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
|
||||
link.click();
|
||||
|
|
@ -171,8 +274,24 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
}
|
||||
|
||||
function getNodeId() {
|
||||
return `dndnode_` + incrementNodeId();
|
||||
function downloadFlows() {
|
||||
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.
|
||||
* The resulting JSON object is passed to the addFlow function.
|
||||
*/
|
||||
function uploadFlow() {
|
||||
function uploadFlow(newProject?: boolean) {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
// add a change event listener to the file input
|
||||
input.onchange = (e: Event) => {
|
||||
// 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
|
||||
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,21 +350,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* @param {string} id - The id of the flow to remove.
|
||||
*/
|
||||
function removeFlow(id: string) {
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
if (index === tabIndex) {
|
||||
setTabIndex(flows.length - 2);
|
||||
newFlows.splice(index, 1);
|
||||
} else {
|
||||
let flowId = flows[tabIndex].id;
|
||||
newFlows.splice(index, 1);
|
||||
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
|
||||
}
|
||||
}
|
||||
return newFlows;
|
||||
});
|
||||
const index = flows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
deleteFlowFromDatabase(id).then(() => {
|
||||
setFlows(flows.filter((flow) => flow.id !== id));
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a new flow to the list of flows.
|
||||
|
|
@ -251,9 +384,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
? { x: position.paneX + position.x, y: position.paneY + position.y }
|
||||
: reactFlowInstance.project({ x: position.x, y: position.y });
|
||||
|
||||
selectionInstance.nodes.forEach((n) => {
|
||||
selectionInstance.nodes.forEach((n: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId();
|
||||
let newId = getNodeId(n.data.type);
|
||||
idsMap[n.id] = newId;
|
||||
|
||||
// Create a new node object
|
||||
|
|
@ -318,67 +451,106 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
reactFlowInstance.setEdges(edges);
|
||||
}
|
||||
|
||||
function addFlow(flow?: FlowType) {
|
||||
// Get data from the flow or set it to null if there's no flow provided.
|
||||
const addFlow = async (
|
||||
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;
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
if (data) {
|
||||
data.edges.forEach((edge) => {
|
||||
edge.style = { stroke: "inherit" };
|
||||
edge.className =
|
||||
edge.targetHandle.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 dark:stroke-gray-300"
|
||||
: "stroke-gray-900 dark:stroke-gray-200";
|
||||
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
||||
});
|
||||
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
|
||||
);
|
||||
}
|
||||
});
|
||||
updateIds(data, getNodeId);
|
||||
updateEdges(data.edges);
|
||||
updateNodes(data.nodes, data.edges);
|
||||
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
}
|
||||
// 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.
|
||||
setId(uuidv4());
|
||||
return { data, description };
|
||||
};
|
||||
|
||||
// Add the new flow to the list of flows.
|
||||
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState, newFlow];
|
||||
return newFlows;
|
||||
const updateEdges = (edges) => {
|
||||
edges.forEach((edge) => {
|
||||
edge.style = { stroke: "inherit" };
|
||||
edge.className =
|
||||
edge.targetHandle.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 dark:stroke-gray-300"
|
||||
: "stroke-gray-900 dark:stroke-gray-200";
|
||||
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
||||
});
|
||||
};
|
||||
|
||||
const updateNodes = (nodes, edges) => {
|
||||
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"];
|
||||
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
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createNewFlow = (flowData, flow) => ({
|
||||
description: flowData.description,
|
||||
name: flow?.name ?? getRandomName(),
|
||||
data: flowData.data,
|
||||
id: "",
|
||||
});
|
||||
|
||||
const addFlowToLocalState = (newFlow) => {
|
||||
setFlows((prevState) => {
|
||||
return [...prevState, newFlow];
|
||||
});
|
||||
};
|
||||
|
||||
// Set the tab index to the new flow.
|
||||
setTabIndex(flows.length);
|
||||
}
|
||||
/**
|
||||
* Updates an existing flow with new data
|
||||
* @param newFlow - The new flow object containing the updated data
|
||||
|
|
@ -395,27 +567,64 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
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);
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
saveFlow,
|
||||
lastCopiedSelection,
|
||||
setLastCopiedSelection,
|
||||
disableCopyPaste,
|
||||
setDisableCopyPaste,
|
||||
save,
|
||||
hardReset,
|
||||
tabIndex,
|
||||
setTabIndex,
|
||||
tabId,
|
||||
setTabId,
|
||||
flows,
|
||||
save,
|
||||
incrementNodeId,
|
||||
removeFlow,
|
||||
addFlow,
|
||||
updateFlow,
|
||||
downloadFlow,
|
||||
downloadFlows,
|
||||
uploadFlows,
|
||||
uploadFlow,
|
||||
getNodeId,
|
||||
tabsState,
|
||||
setTabsState,
|
||||
paste,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -51,16 +51,21 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
);
|
||||
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
||||
setTypes(
|
||||
Object.keys(result.data).reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {})
|
||||
// Reverse the keys so the tool world does not overlap
|
||||
Object.keys(result.data)
|
||||
.reverse()
|
||||
.reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach(
|
||||
(c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
}
|
||||
);
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
// Clear the interval if successful.
|
||||
|
|
|
|||
|
|
@ -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 { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { TabsContext } from "./tabsContext";
|
||||
|
||||
type undoRedoContextType = {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
takeSnapshot: () => void;
|
||||
};
|
||||
|
||||
type UseUndoRedoOptions = {
|
||||
maxHistorySize: number;
|
||||
|
|
@ -21,27 +33,34 @@ type HistoryItem = {
|
|||
edges: Edge[];
|
||||
};
|
||||
|
||||
const initialValue = {
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
takeSnapshot: () => {},
|
||||
};
|
||||
|
||||
const defaultOptions: UseUndoRedoOptions = {
|
||||
maxHistorySize: 100,
|
||||
enableShortcuts: true,
|
||||
};
|
||||
|
||||
// https://redux.js.org/usage/implementing-undo-history
|
||||
export const useUndoRedo: UseUndoRedo = ({
|
||||
maxHistorySize = defaultOptions.maxHistorySize,
|
||||
enableShortcuts = defaultOptions.enableShortcuts,
|
||||
} = defaultOptions) => {
|
||||
// the past and future arrays store the states that we can jump to
|
||||
const { tabIndex, flows } = useContext(TabsContext);
|
||||
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
|
||||
|
||||
export function UndoRedoProvider({ children }) {
|
||||
const { tabId, flows } = useContext(TabsContext);
|
||||
|
||||
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
const [tabIndex, setTabIndex] = useState(
|
||||
flows.findIndex((f) => f.id === tabId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 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] : [])));
|
||||
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();
|
||||
|
||||
|
|
@ -50,7 +69,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
setPast((old) => {
|
||||
let newPast = cloneDeep(old);
|
||||
newPast[tabIndex] = old[tabIndex].slice(
|
||||
old[tabIndex].length - maxHistorySize + 1,
|
||||
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
|
||||
old[tabIndex].length
|
||||
);
|
||||
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
||||
|
|
@ -63,16 +82,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
newFuture[tabIndex] = [];
|
||||
return newFuture;
|
||||
});
|
||||
}, [
|
||||
getNodes,
|
||||
getEdges,
|
||||
past,
|
||||
future,
|
||||
tabIndex,
|
||||
setPast,
|
||||
setFuture,
|
||||
maxHistorySize,
|
||||
]);
|
||||
}, [getNodes, getEdges, past, future, flows, tabId, setPast, setFuture]);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
// get the last state that we want to go back to
|
||||
|
|
@ -141,7 +151,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
if (!enableShortcuts) {
|
||||
if (!defaultOptions.enableShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,15 +175,16 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
return () => {
|
||||
document.removeEventListener("keydown", keyDownHandler);
|
||||
};
|
||||
}, [undo, redo, enableShortcuts]);
|
||||
|
||||
return {
|
||||
undo,
|
||||
redo,
|
||||
takeSnapshot,
|
||||
canUndo: !!past.length,
|
||||
canRedo: !!future.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUndoRedo;
|
||||
}, [undo, redo]);
|
||||
return (
|
||||
<undoRedoContext.Provider
|
||||
value={{
|
||||
undo,
|
||||
redo,
|
||||
takeSnapshot,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</undoRedoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 axios, { AxiosResponse } from "axios";
|
||||
import { FlowType } from "../../types/flow";
|
||||
|
||||
// when serving with static files
|
||||
// We need to add /api/v1/ to the url in the axios calls
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
|
||||
/**
|
||||
* Retrieves all data from the API.
|
||||
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse object containing the API data.
|
||||
* Fetches all objects from the API endpoint.
|
||||
*
|
||||
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
|
||||
*/
|
||||
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
|
||||
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) {
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
template: string
|
||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||
|
|
@ -38,19 +54,10 @@ export async function checkPrompt(
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves the version of the API.
|
||||
* @returns {Promise<AxiosResponse<{ version: string }>>} A promise that resolves to an AxiosResponse object containing the API version.
|
||||
* @example
|
||||
* const response = await getVersion();
|
||||
* console.log(response.data.version);
|
||||
* // 0.1.0
|
||||
* Fetches a list of JSON files from a GitHub repository and returns their contents as an array of FlowType objects.
|
||||
*
|
||||
* @returns {Promise<FlowType[]>} A promise that resolves to an array of FlowType objects.
|
||||
*/
|
||||
export async function getVersion(): Promise<
|
||||
AxiosResponse<{ version: string }>
|
||||
> {
|
||||
return await axios.get("/api/v1/version");
|
||||
}
|
||||
|
||||
export async function getExamples(): Promise<FlowType[]> {
|
||||
const url =
|
||||
"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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
--background: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--muted: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
--card: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--border: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
--input: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--primary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
|
||||
--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;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
--background: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||
--foreground: 213 31% 91%; /* hsl(213 31% 91%) */
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
--muted: 223 47% 11%; /* hsl(223 47% 11%) */
|
||||
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
--card: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||
--card-foreground: 213 31% 91%; /* hsl(213 31% 91%) */
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
--border: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
--input: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
--primary: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--primary-foreground: 222.2 47.4% 1.2%; /* hsl(222 47% 1%) */
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--secondary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--secondary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
--accent-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
|
||||
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--ring: 216 34% 17%;
|
||||
--ring: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--ring: 215 20.2% 65.1%;
|
||||
--background: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
|
||||
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--card: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
|
||||
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
|
||||
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
|
||||
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
-background: 224 71% 4%;
|
||||
/* hsl(224 71% 4%) */
|
||||
-foreground: 213 31% 91%;
|
||||
/* hsl(213 31% 91%) */
|
||||
-muted: 223 47% 11%;
|
||||
/* hsl(223 47% 11%) */
|
||||
-muted-foreground: 215.4 16.3% 56.9%;
|
||||
/* hsl(215 16% 56%) */
|
||||
-popover: 224 71% 4%;
|
||||
/* hsl(224 71% 4%) */
|
||||
-popover-foreground: 215 20.2% 65.1%;
|
||||
/* hsl(215 20% 65%) */
|
||||
-card: 224 71% 4%;
|
||||
/* hsl(224 71% 4%) */
|
||||
-card-foreground: 213 31% 91%;
|
||||
/* hsl(213 31% 91%) */
|
||||
-border: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-input: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-primary: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-primary-foreground: 222.2 47.4% 1.2%;
|
||||
/* hsl(222 47% 1%) */
|
||||
-secondary: 222.2 47.4% 11.2%;
|
||||
/* hsl(222 47% 11%) */
|
||||
-secondary-foreground: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-accent: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-accent-foreground: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-destructive: 0 63% 31%;
|
||||
/* hsl(0 63% 31%) */
|
||||
-destructive-foreground: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-ring: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-radius: 0.5rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||
import {
|
||||
XMarkIcon,
|
||||
CommandLineIcon,
|
||||
CodeBracketSquareIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { CodeBracketSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
|
|
@ -18,18 +13,26 @@ import {
|
|||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} 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 { dark } = useContext(darkContext);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
|
|
@ -52,27 +55,18 @@ export default function ApiModal({ flowName }) {
|
|||
}
|
||||
}
|
||||
|
||||
const pythonApiCode = `import requests
|
||||
import json
|
||||
const pythonApiCode = getPythonApiCode(flow);
|
||||
|
||||
API_URL = "${window.location.protocol}//${window.location.host}/predict"
|
||||
|
||||
def predict(message):
|
||||
with open("${flowName}.json", "r") as f:
|
||||
json_data = json.load(f)
|
||||
payload = {'exported_flow': json_data, 'message': message}
|
||||
response = requests.post(API_URL, json=payload)
|
||||
return response.json() # JSON {"result": "Response"}
|
||||
|
||||
print(predict("Your message"))`;
|
||||
|
||||
const pythonCode = `from langflow import load_flow_from_json
|
||||
|
||||
flow = load_flow_from_json("${flowName}.json")
|
||||
# Now you can use it like any chain
|
||||
flow("Hey, have you heard of LangFlow?")`;
|
||||
const curl_code = getCurlCode(flow);
|
||||
const pythonCode = getPythonCode(flow);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
code: curl_code,
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
|
|
@ -90,7 +84,7 @@ flow("Hey, have you heard of LangFlow?")`;
|
|||
return (
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<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>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Code</span>
|
||||
|
|
@ -99,57 +93,46 @@ flow("Hey, have you heard of LangFlow?")`;
|
|||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Export your flow to use it with this code.
|
||||
</DialogDescription>
|
||||
<DialogDescription>{EXPORT_CODE_DIALOG}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col h-full w-full ">
|
||||
<div className="flex px-5 z-10">
|
||||
{tabs.map((tab, index) => (
|
||||
<Tabs
|
||||
defaultValue={"0"}
|
||||
className="w-full h-full overflow-hidden text-center bg-muted rounded-md border"
|
||||
onValueChange={(value) => setActiveTab(value)}
|
||||
>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<TabsList>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsTrigger value={index.toString()}>{tab.name}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="float-right">
|
||||
<button
|
||||
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")
|
||||
}
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{tab.name}
|
||||
<img src={tab.image} className="w-6" />
|
||||
{isCopied ? <Check size={18} /> : <Clipboard size={15} />}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<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">
|
||||
<button
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck size={18} />
|
||||
) : (
|
||||
<IconClipboard size={18} />
|
||||
)}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
className="h-[370px] w-full"
|
||||
language={tabs[activeTab].mode}
|
||||
style={oneDark}
|
||||
customStyle={{ margin: 0 }}
|
||||
>
|
||||
{tabs[activeTab].code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsContent
|
||||
value={index.toString()}
|
||||
className="overflow-hidden w-full h-full px-4 pb-4 -mt-1"
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
className="h-[400px] w-full overflow-auto"
|
||||
language={tab.mode}
|
||||
style={oneDark}
|
||||
>
|
||||
{tab.code}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,37 +1,27 @@
|
|||
import {
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronDoubleRightIcon,
|
||||
PencilSquareIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { classNames, limitScrollFieldsModal, nodeIcons } from "../../utils";
|
||||
import { classNames, limitScrollFieldsModal } from "../../utils";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import ToggleShadComponent from "../../components/toggleShadComponent";
|
||||
import { VariableIcon } from "@heroicons/react/24/outline";
|
||||
import InputListComponent from "../../components/inputListComponent";
|
||||
import TextAreaComponent from "../../components/textAreaComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import ToggleComponent from "../../components/toggleComponent";
|
||||
import FloatComponent from "../../components/floatComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
import IntComponent from "../../components/intComponent";
|
||||
import InputFileComponent from "../../components/inputFileComponent";
|
||||
import PromptAreaComponent from "../../components/promptComponent";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -42,7 +32,7 @@ import {
|
|||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
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 }) {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
|
@ -60,12 +50,11 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
data.node.template[t].type === "int")
|
||||
).length
|
||||
);
|
||||
const [nodeValue, setNodeValue] = useState(true);
|
||||
const [nodeValue, setNodeValue] = useState(null);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const { types } = useContext(typesContext);
|
||||
const ref = useRef();
|
||||
const { save } = useContext(TabsContext);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [enabled, setEnabled] = useState(null);
|
||||
if (nodeLength == 0) {
|
||||
closePopUp();
|
||||
}
|
||||
|
|
@ -77,6 +66,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
}
|
||||
}
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
|
||||
function changeAdvanced(node): void {
|
||||
Object.keys(data.node.template).filter((n, i) => {
|
||||
if (n === node.name) {
|
||||
|
|
@ -87,37 +78,32 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
setNodeValue(!nodeValue);
|
||||
}
|
||||
|
||||
// console.log(data.node.template);
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[700px]">
|
||||
<DialogContent className="lg:max-w-[700px] ">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Edit Node</span>
|
||||
<PencilSquareIcon
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="pr-2">{data.type}</span>
|
||||
<Badge variant="secondary">ID: {data.id}</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{EDIT_DIALOG_SUBTITLE}
|
||||
<div className="flex pt-3">
|
||||
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2">
|
||||
{data.node?.description}
|
||||
<div className="flex pt-4">
|
||||
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2 dark:text-slate-200">
|
||||
|
||||
</VariableIcon>
|
||||
<span className="text-sm font-semibold text-gray-800">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-white">
|
||||
Parameters
|
||||
</span>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex w-full h-fit max-h-[415px]">
|
||||
<div className="flex w-full max-h-[350px] h-fit">
|
||||
<div
|
||||
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
|
||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||
: "overflow-hidden"
|
||||
|
|
@ -125,9 +111,9 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
>
|
||||
{nodeLength > 0 && (
|
||||
<div className="flex flex-col gap-5 h-fit">
|
||||
<Table className="table-fixed">
|
||||
<TableHeader className="border-gray-200 text-gray-500 text-xs font-medium">
|
||||
<TableRow>
|
||||
<Table className="table-fixed bg-muted outline-1">
|
||||
<TableHeader className="border-gray-200 text-gray-500 text-xs font-medium h-10">
|
||||
<TableRow className="dark:border-b-muted">
|
||||
<TableHead className="h-7 text-center">PARAM</TableHead>
|
||||
<TableHead className="p-0 h-7 text-center">
|
||||
VALUE
|
||||
|
|
@ -150,8 +136,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
data.node.template[t].type === "int")
|
||||
)
|
||||
.map((n, i) => (
|
||||
<TableRow key={i} className="h-8">
|
||||
<TableCell className="p-0 text-center text-gray-900 text-xs dark:text-gray-300">
|
||||
<TableRow key={i} className="h-10 dark:border-b-muted">
|
||||
<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].display_name}
|
||||
|
|
@ -162,6 +148,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
<div className="mx-auto">
|
||||
{data.node.template[n].list ? (
|
||||
<InputListComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={
|
||||
!data.node.template[n].value ||
|
||||
|
|
@ -171,7 +158,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
) : data.node.template[n].multiline ? (
|
||||
|
|
@ -181,7 +167,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -194,7 +179,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -207,8 +191,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
setEnabled={(e) => {
|
||||
data.node.template[n].value = e;
|
||||
setEnabled(e);
|
||||
save();
|
||||
}}
|
||||
size="small"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -220,7 +204,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -228,6 +211,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
data.node.template[n].options ? (
|
||||
<div className="mx-auto">
|
||||
<Dropdown
|
||||
numberOfOptions={nodeLength}
|
||||
editNode={true}
|
||||
options={data.node.template[n].options}
|
||||
onSelect={(newValue) =>
|
||||
|
|
@ -247,7 +231,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -264,7 +247,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
suffixes={data.node.template[n].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[n].content = t;
|
||||
save();
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
|
|
@ -276,7 +258,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -288,7 +269,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -306,6 +286,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
changeAdvanced(data.node.template[n])
|
||||
}
|
||||
disabled={false}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -326,7 +307,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
|||
}}
|
||||
type="submit"
|
||||
>
|
||||
Save changes
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export default function ModalField({
|
|||
type,
|
||||
index,
|
||||
}) {
|
||||
const { save } = useContext(TabsContext);
|
||||
const [enabled, setEnabled] = useState(
|
||||
data.node.template[name]?.value ?? false
|
||||
);
|
||||
|
|
@ -71,7 +70,6 @@ export default function ModalField({
|
|||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
|
|
@ -80,7 +78,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -90,7 +87,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -104,7 +100,6 @@ export default function ModalField({
|
|||
setEnabled={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
setEnabled(t);
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -115,7 +110,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -134,7 +128,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -150,7 +143,6 @@ export default function ModalField({
|
|||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[name].content = t;
|
||||
save();
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
|
|
@ -161,7 +153,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -172,7 +163,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</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">
|
||||
<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={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
|||
import { classNames } from "../../../utils";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
|
||||
import { INPUT_STYLE } from "../../../constants";
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
|
|
@ -44,9 +44,10 @@ export default function ChatInput({
|
|||
}}
|
||||
className={classNames(
|
||||
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",
|
||||
"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..."}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ export default function ChatMessage({
|
|||
className={classNames(
|
||||
"w-full py-2 pl-2 flex",
|
||||
chat.isSend
|
||||
? "bg-white dark:bg-gray-900 "
|
||||
: "bg-gray-200 dark:bg-gray-800"
|
||||
? "bg-background dark:bg-gray-900 "
|
||||
: "bg-input dark:bg-gray-800"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -80,7 +80,7 @@ export default function ChatMessage({
|
|||
<div
|
||||
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
|
||||
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={{
|
||||
__html: convert.toHtml(chat.thought),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function FileCard({ fileName, content, fileType }) {
|
|||
/>
|
||||
{isHovered && (
|
||||
<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
|
||||
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 (
|
||||
<button
|
||||
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">
|
||||
{" "}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
ChatBubbleOvalLeftEllipsisIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
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 { toNormalCase } from "../../utils";
|
||||
import { validateNodes } from "../../utils";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import ChatMessage from "./chatMessage";
|
||||
import { FaEraser } from "react-icons/fa";
|
||||
import { HiX } from "react-icons/hi";
|
||||
import { Eraser } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
import { sendAllProps } from "../../types/api";
|
||||
import { ChatMessageType, ChatType } from "../../types/chat";
|
||||
import { ChatMessageType } from "../../types/chat";
|
||||
import ChatInput from "./chatInput";
|
||||
|
||||
import _, { set } from "lodash";
|
||||
import _ from "lodash";
|
||||
|
||||
export default function ChatModal({
|
||||
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) {
|
||||
if (Array.isArray(data)) {
|
||||
//set chat history
|
||||
|
|
@ -180,12 +188,10 @@ export default function ChatModal({
|
|||
|
||||
function connectWS() {
|
||||
try {
|
||||
const urlWs =
|
||||
const urlWs = getWebSocketUrl(
|
||||
id.current,
|
||||
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);
|
||||
newWs.onopen = () => {
|
||||
console.log("WebSocket connection established!");
|
||||
|
|
@ -264,54 +270,6 @@ export default function ChatModal({
|
|||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatHistory]);
|
||||
|
||||
function validateNode(n: NodeType): Array<string> {
|
||||
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
|
||||
setNoticeData({
|
||||
title:
|
||||
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const {
|
||||
type,
|
||||
node: { template },
|
||||
} = n.data;
|
||||
|
||||
return Object.keys(template).reduce(
|
||||
(errors: Array<string>, t) =>
|
||||
errors.concat(
|
||||
template[t].required &&
|
||||
template[t].show &&
|
||||
(template[t].value === 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);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -322,7 +280,7 @@ export default function ChatModal({
|
|||
|
||||
function sendMessage() {
|
||||
if (chatValue !== "") {
|
||||
let nodeValidationErrors = validateNodes();
|
||||
let nodeValidationErrors = validateNodes(reactFlowInstance);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
setLockChat(true);
|
||||
let message = chatValue;
|
||||
|
|
@ -394,13 +352,13 @@ export default function ChatModal({
|
|||
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"
|
||||
>
|
||||
<FaEraser className="w-4 h-4" />
|
||||
<Eraser className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<HiX className="w-5 h-5" />
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -425,7 +383,7 @@ export default function ChatModal({
|
|||
</span>
|
||||
</span>
|
||||
<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">
|
||||
Start a conversation and click the agent’s thoughts{" "}
|
||||
<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