Adding API improvements, HomePage and validation improvements (#493)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-06-16 19:17:13 -03:00 • committed by GitHub
commit 0f59382704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
132 changed files with 16319 additions and 11981 deletions

1
.gitignore vendored
View file

@ -241,3 +241,4 @@ dmypy.json
# Poetry # Poetry
.testenv/* .testenv/*
langflow.db

2
.vscode/launch.json vendored
View file

@ -35,7 +35,7 @@
"name": "Debug Frontend", "name": "Debug Frontend",
"type": "chrome", "type": "chrome",
"request": "launch", "request": "launch",
"url": "http://localhost:3000/*", "url": "http://localhost:3000/",
"webRoot": "${workspaceRoot}/src/frontend" "webRoot": "${workspaceRoot}/src/frontend"
} }
] ]

629
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -29,10 +29,9 @@ google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0" google-api-python-client = "^2.79.0"
typer = "^0.7.0" typer = "^0.7.0"
gunicorn = "^20.1.0" gunicorn = "^20.1.0"
langchain = "^0.0.194" langchain = "^0.0.200"
openai = "^0.27.7" openai = "^0.27.8"
types-pyyaml = "^6.0.12.8" types-pyyaml = "^6.0.12.8"
dill = "^0.3.6"
pandas = "^1.5.3" pandas = "^1.5.3"
chromadb = "^0.3.21" chromadb = "^0.3.21"
huggingface-hub = "^0.13.3" huggingface-hub = "^0.13.3"
@ -57,9 +56,13 @@ jina = "3.15.2"
sentence-transformers = "^2.2.2" sentence-transformers = "^2.2.2"
ctransformers = "^0.2.2" ctransformers = "^0.2.2"
cohere = "^4.6.0" cohere = "^4.6.0"
sqlmodel = "^0.0.8"
faiss-cpu = "^1.7.4" faiss-cpu = "^1.7.4"
anthropic = "^0.2.9" anthropic = "^0.2.9"
orjson = "^3.9.0"
multiprocess = "^0.70.14" multiprocess = "^0.70.14"
cachetools = "^5.3.1"
types-cachetools = "^5.3.0.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

View file

@ -1,16 +1,18 @@
import sys import sys
import time import time
from fastapi import FastAPI
import httpx import httpx
from multiprocess import Process, cpu_count # type: ignore from multiprocess import Process, cpu_count # type: ignore
import platform import platform
from pathlib import Path from pathlib import Path
from typing import Optional
import socket
from rich.panel import Panel from rich.panel import Panel
from rich import box from rich import box
from rich import print as rprint from rich import print as rprint
import typer import typer
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from langflow.main import create_app from langflow.main import create_app
from langflow.settings import settings from langflow.settings import settings
from langflow.utils.logger import configure, logger from langflow.utils.logger import configure, logger
@ -26,10 +28,19 @@ def get_number_of_workers(workers=None):
return workers return workers
def update_settings(config: str, dev: bool = False): def update_settings(
config: str,
dev: bool = False,
database_url: Optional[str] = None,
remove_api_keys: bool = False,
):
"""Update the settings from a config file.""" """Update the settings from a config file."""
if config: if config:
settings.update_from_yaml(config, dev=dev) settings.update_from_yaml(config, dev=dev)
if database_url:
settings.update_settings(database_url=database_url)
if remove_api_keys:
settings.update_settings(remove_api_keys=remove_api_keys)
def serve_on_jcloud(): def serve_on_jcloud():
@ -93,6 +104,10 @@ def serve(
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."), log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"), jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"), dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
database_url: str = typer.Option(
None,
help="Database URL to connect to. If not provided, a local SQLite database will be used.",
),
path: str = typer.Option( path: str = typer.Option(
None, None,
help="Path to the frontend directory containing build files. This is for development purposes only.", help="Path to the frontend directory containing build files. This is for development purposes only.",
@ -100,23 +115,12 @@ def serve(
open_browser: bool = typer.Option( open_browser: bool = typer.Option(
True, help="Open the browser after starting the server." True, help="Open the browser after starting the server."
), ),
remove_api_keys: bool = typer.Option(
False, help="Remove API keys from the projects saved in the database."
),
): ):
""" """
Run the Langflow server. Run the Langflow server.
Args:
host (str): Host to bind the server to.
workers (int): Number of worker processes.
timeout (int): Worker timeout in seconds.
port (int): Port to listen on.
config (str): Path to the configuration file.
env_file (Path): Path to the .env file containing environment variables.
log_level (str): Logging level.
log_file (Path): Path to the log file.
jcloud (bool): Deploy on Jina AI Cloud.
dev (bool): Run in development mode (may contain bugs).
path (str): Path to the frontend directory containing build files. This is for development purposes only.
open_browser (bool): Open the browser after starting the server.
""" """
if jcloud: if jcloud:
@ -125,19 +129,22 @@ def serve(
load_dotenv(env_file) load_dotenv(env_file)
configure(log_level=log_level, log_file=log_file) configure(log_level=log_level, log_file=log_file)
update_settings(config, dev=dev) update_settings(
app = create_app() config, dev=dev, database_url=database_url, remove_api_keys=remove_api_keys
)
# get the directory of the current file # get the directory of the current file
if not path: if not path:
frontend_path = Path(__file__).parent frontend_path = Path(__file__).parent
static_files_dir = frontend_path / "frontend" static_files_dir = frontend_path / "frontend"
else: else:
static_files_dir = Path(path) static_files_dir = Path(path)
app.mount(
"/", app = create_app()
StaticFiles(directory=static_files_dir, html=True), setup_static_files(app, static_files_dir)
name="static", # check if port is being used
) if is_port_in_use(port, host):
port = get_free_port(port)
options = { options = {
"bind": f"{host}:{port}", "bind": f"{host}:{port}",
"workers": get_number_of_workers(workers), "workers": get_number_of_workers(workers),
@ -152,7 +159,8 @@ def serve(
status_code = 0 status_code = 0
while status_code != 200: while status_code != 200:
try: try:
status_code = httpx.get(f"http://{host}:{port}").status_code status_code = httpx.get(f"http://{host}:{port}/health").status_code
except Exception: except Exception:
time.sleep(1) time.sleep(1)
@ -161,11 +169,64 @@ def serve(
webbrowser.open(f"http://{host}:{port}") webbrowser.open(f"http://{host}:{port}")
def setup_static_files(app: FastAPI, static_files_dir: Path):
"""
Setup the static files directory.
Args:
app (FastAPI): FastAPI app.
path (str): Path to the static files directory.
"""
app.mount(
"/",
StaticFiles(directory=static_files_dir, html=True),
name="static",
)
@app.exception_handler(404)
async def custom_404_handler(request, __):
path = static_files_dir / "index.html"
if not path.exists():
raise RuntimeError(f"File at path {path} does not exist.")
return FileResponse(path)
def is_port_in_use(port, host="localhost"):
"""
Check if a port is in use.
Args:
port (int): The port number to check.
host (str): The host to check the port on. Defaults to 'localhost'.
Returns:
bool: True if the port is in use, False otherwise.
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex((host, port)) == 0
def get_free_port(port):
"""
Given a used port, find a free port.
Args:
port (int): The port number to check.
Returns:
int: A free port number.
"""
while is_port_in_use(port):
port += 1
return port
def print_banner(host, port): def print_banner(host, port):
# console = Console() # console = Console()
word = "LangFlow" word = "LangFlow"
colors = ["#690080", "#660099", "#4d00b3", "#3300cc", "#1a00e6", "#0000ff"] colors = ["#3300cc"]
styled_word = "" styled_word = ""

View file

@ -1,8 +1,18 @@
# Router for base api # Router for base api
from fastapi import APIRouter from fastapi import APIRouter
from langflow.api.v1 import chat_router, endpoints_router, validate_router from langflow.api.v1 import (
chat_router,
endpoints_router,
validate_router,
flows_router,
flow_styles_router,
)
router = APIRouter(prefix="/api/v1", tags=["api"]) router = APIRouter(
prefix="/api/v1",
)
router.include_router(chat_router) router.include_router(chat_router)
router.include_router(endpoints_router) router.include_router(endpoints_router)
router.include_router(validate_router) router.include_router(validate_router)
router.include_router(flows_router)
router.include_router(flow_styles_router)

View 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

View file

@ -1,5 +1,13 @@
from langflow.api.v1.endpoints import router as endpoints_router from langflow.api.v1.endpoints import router as endpoints_router
from langflow.api.v1.validate import router as validate_router from langflow.api.v1.validate import router as validate_router
from langflow.api.v1.chat import router as chat_router from langflow.api.v1.chat import router as chat_router
from langflow.api.v1.flows import router as flows_router
from langflow.api.v1.flow_styles import router as flow_styles_router
__all__ = ["chat_router", "endpoints_router", "validate_router"] __all__ = [
"chat_router",
"endpoints_router",
"validate_router",
"flows_router",
"flow_styles_router",
]

View file

@ -1,26 +1,122 @@
import json
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
HTTPException,
WebSocket, WebSocket,
WebSocketDisconnect,
WebSocketException, WebSocketException,
status, status,
) )
from fastapi.responses import StreamingResponse
from langflow.api.v1.schemas import BuiltResponse, InitResponse
from langflow.chat.manager import ChatManager from langflow.chat.manager import ChatManager
from langflow.graph.graph.base import Graph
from langflow.utils.logger import logger from langflow.utils.logger import logger
from cachetools import LRUCache
router = APIRouter() router = APIRouter(tags=["Chat"])
chat_manager = ChatManager() chat_manager = ChatManager()
flow_data_store: LRUCache = LRUCache(maxsize=10)
@router.websocket("/chat/{client_id}") @router.websocket("/chat/{client_id}")
async def websocket_endpoint(client_id: str, websocket: WebSocket): async def chat(client_id: str, websocket: WebSocket):
"""Websocket endpoint for chat.""" """Websocket endpoint for chat."""
try: try:
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: except WebSocketException as exc:
logger.error(exc) logger.error(exc)
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc)) await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
except WebSocketDisconnect as exc:
@router.post("/build/init", response_model=InitResponse, status_code=201)
async def init_build(graph_data: dict):
"""Initialize the build by storing graph data and returning a unique session ID."""
try:
flow_id = graph_data.get("id")
if flow_id is None:
raise ValueError("No ID provided")
flow_data_store[flow_id] = graph_data
return InitResponse(flowId=flow_id)
except Exception as exc:
logger.error(exc) logger.error(exc)
await websocket.close(code=status.WS_1000_NORMAL_CLOSURE, reason=str(exc)) return HTTPException(status_code=500, detail=str(exc))
@router.get("/build/{flow_id}/status", response_model=BuiltResponse)
async def build_status(flow_id: str):
"""Check the flow_id is in the flow_data_store."""
try:
built = flow_id in flow_data_store and not isinstance(
flow_data_store[flow_id], dict
)
return BuiltResponse(
built=built,
)
except Exception as exc:
logger.error(exc)
return HTTPException(status_code=500, detail=str(exc))
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
async def stream_build(flow_id: str):
"""Stream the build process based on stored flow data."""
async def event_stream(flow_id):
final_response = json.dumps({"end_of_stream": True})
try:
if flow_id not in flow_data_store:
error_message = "Invalid session ID"
yield f"data: {json.dumps({'error': error_message})}\n\n"
return
graph_data = flow_data_store[flow_id].get("data")
if not graph_data:
error_message = "No data provided"
yield f"data: {json.dumps({'error': error_message})}\n\n"
return
logger.debug("Building langchain object")
graph = Graph.from_payload(graph_data)
for node in graph.generator_build():
try:
node.build()
params = node._built_object_repr()
valid = True
logger.debug(
f"Building node {params[:50]}{'...' if len(params) > 50 else ''}"
)
except Exception as exc:
params = str(exc)
valid = False
response = json.dumps(
{
"valid": valid,
"params": params,
"id": node.id,
}
)
yield f"data: {response}\n\n"
chat_manager.set_cache(flow_id, graph.build())
except Exception as exc:
logger.error("Error while building the flow: %s", exc)
yield f"error: {json.dumps({'error': str(exc)})}\n\n"
finally:
yield f"data: {final_response}\n\n"
try:
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")
except Exception as exc:
logger.error(exc)
raise HTTPException(status_code=500, detail=str(exc))

View file

@ -1,20 +1,20 @@
import logging from langflow.database.models.flow import Flow
from importlib.metadata import version from langflow.processing.process import process_graph_cached, process_tweaks
from langflow.utils.logger import logger
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, Depends, HTTPException
from langflow.api.v1.schemas import ( from langflow.api.v1.schemas import (
ExportedFlow,
GraphData,
PredictRequest, PredictRequest,
PredictResponse, PredictResponse,
) )
from langflow.interface.types import build_langchain_types_dict from langflow.interface.types import build_langchain_types_dict
from langflow.database.base import get_session
from sqlmodel import Session
# build router # build router
router = APIRouter() router = APIRouter(tags=["Base"])
logger = logging.getLogger(__name__)
@router.get("/all") @router.get("/all")
@ -22,16 +22,34 @@ def get_all():
return build_langchain_types_dict() return build_langchain_types_dict()
@router.post("/predict", response_model=PredictResponse) @router.post("/predict/{flow_id}", response_model=PredictResponse)
async def get_load(predict_request: PredictRequest): async def predict_flow(
try: predict_request: PredictRequest,
from langflow.processing.process import process_graph_cached flow_id: str,
session: Session = Depends(get_session),
):
"""
Endpoint to process a message using the flow passed in the bearer token.
"""
exported_flow: ExportedFlow = predict_request.exported_flow try:
graph_data: GraphData = exported_flow.data flow = session.get(Flow, flow_id)
data = graph_data.dict() if flow is None:
response = process_graph_cached(data, predict_request.message) raise ValueError(f"Flow {flow_id} not found")
return PredictResponse(result=response.get("result", ""))
if flow.data is None:
raise ValueError(f"Flow {flow_id} has no data")
graph_data = flow.data
if predict_request.tweaks:
try:
graph_data = process_tweaks(graph_data, predict_request.tweaks)
except Exception as exc:
logger.error(f"Error processing tweaks: {exc}")
response = process_graph_cached(graph_data, predict_request.message)
return PredictResponse(
result=response.get("result", ""),
intermediate_steps=response.get("thought", ""),
)
except Exception as e: except Exception as e:
# Log stack trace # Log stack trace
logger.exception(e) logger.exception(e)

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

View 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)

View file

@ -1,6 +1,6 @@
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Optional, Union
from langflow.database.models.flow import FlowCreate, FlowRead
from pydantic import BaseModel, validator from pydantic import BaseModel, Field, validator
class GraphData(BaseModel): class GraphData(BaseModel):
@ -23,13 +23,30 @@ class PredictRequest(BaseModel):
"""Predict request schema.""" """Predict request schema."""
message: str message: str
exported_flow: ExportedFlow tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
class Config:
schema_extra = {
"example": {
"message": "Hello, how are you?",
"tweaks": {
"dndnode_986363f0-4677-4035-9f38-74b94af5dd78": {
"name": "A tool name",
"description": "A tool description",
},
"dndnode_986363f0-4677-4035-9f38-74b94af57378": {
"template": "A {template}",
},
},
}
}
class PredictResponse(BaseModel): class PredictResponse(BaseModel):
"""Predict response schema.""" """Predict response schema."""
result: str result: str
intermediate_steps: str = ""
class ChatMessage(BaseModel): class ChatMessage(BaseModel):
@ -68,3 +85,19 @@ class FileResponse(ChatMessage):
if v not in ["image", "csv"]: if v not in ["image", "csv"]:
raise ValueError("data_type must be image or csv") raise ValueError("data_type must be image or csv")
return v return v
class FlowListCreate(BaseModel):
flows: List[FlowCreate]
class FlowListRead(BaseModel):
flows: List[FlowRead]
class InitResponse(BaseModel):
flowId: str
class BuiltResponse(BaseModel):
built: bool

View file

@ -15,7 +15,7 @@ from langflow.utils.logger import logger
from langflow.utils.validate import validate_code from langflow.utils.validate import validate_code
# build router # build router
router = APIRouter(prefix="/validate", tags=["validate"]) router = APIRouter(prefix="/validate", tags=["Validate"])
@router.post("/code", status_code=200, response_model=CodeValidationResponse) @router.post("/code", status_code=200, response_model=CodeValidationResponse)

View file

@ -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",
]

View file

@ -1,154 +1,87 @@
import base64 import abc
import contextlib
import functools
import hashlib
import json
import os
import tempfile
from collections import OrderedDict
from pathlib import Path
from typing import Any, Dict
import dill # type: ignore
CACHE: Dict[str, Any] = {}
def create_cache_folder(func): class BaseCache(abc.ABC):
def wrapper(*args, **kwargs):
# Get the destination folder
cache_path = Path(tempfile.gettempdir()) / PREFIX
# Create the destination folder if it doesn't exist
os.makedirs(cache_path, exist_ok=True)
return func(*args, **kwargs)
return wrapper
def memoize_dict(maxsize=128):
cache = OrderedDict()
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
hashed = compute_dict_hash(args[0])
key = (func.__name__, hashed, frozenset(kwargs.items()))
if key not in cache:
result = func(*args, **kwargs)
cache[key] = result
if len(cache) > maxsize:
cache.popitem(last=False)
else:
result = cache[key]
return result
def clear_cache():
cache.clear()
wrapper.clear_cache = clear_cache # type: ignore
wrapper.cache = cache # type: ignore
return wrapper
return decorator
PREFIX = "langflow_cache"
@create_cache_folder
def clear_old_cache_files(max_cache_size: int = 3):
cache_dir = Path(tempfile.gettempdir()) / PREFIX
cache_files = list(cache_dir.glob("*.dill"))
if len(cache_files) > max_cache_size:
cache_files_sorted_by_mtime = sorted(
cache_files, key=lambda x: x.stat().st_mtime, reverse=True
)
for cache_file in cache_files_sorted_by_mtime[max_cache_size:]:
with contextlib.suppress(OSError):
os.remove(cache_file)
def compute_dict_hash(graph_data):
graph_data = filter_json(graph_data)
cleaned_graph_json = json.dumps(graph_data, sort_keys=True)
return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest()
def filter_json(json_data):
filtered_data = json_data.copy()
# Remove 'viewport' and 'chatHistory' keys
if "viewport" in filtered_data:
del filtered_data["viewport"]
if "chatHistory" in filtered_data:
del filtered_data["chatHistory"]
# Filter nodes
if "nodes" in filtered_data:
for node in filtered_data["nodes"]:
if "position" in node:
del node["position"]
if "positionAbsolute" in node:
del node["positionAbsolute"]
if "selected" in node:
del node["selected"]
if "dragging" in node:
del node["dragging"]
return filtered_data
@create_cache_folder
def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str:
""" """
Save a binary file to the specified folder. Abstract base class for a cache.
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 @abc.abstractmethod
cache_path = Path(tempfile.gettempdir()) / PREFIX def get(self, key):
if not content: """
raise ValueError("Please, reload the file in the loader.") Retrieve an item from the cache.
data = content.split(",")[1]
decoded_bytes = base64.b64decode(data)
# Create the full file path Args:
file_path = os.path.join(cache_path, file_name) key: The key of the item to retrieve.
# Save the binary content to the file Returns:
with open(file_path, "wb") as file: The value associated with the key, or None if the key is not found.
file.write(decoded_bytes) """
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 @abc.abstractmethod
def save_cache(hash_val: str, chat_data, clean_old_cache_files: bool): def delete(self, key):
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill" """
with cache_path.open("wb") as cache_file: Remove an item from the cache.
dill.dump(chat_data, cache_file)
if clean_old_cache_files: Args:
clear_old_cache_files() key: The key of the item to remove.
"""
@abc.abstractmethod
def clear(self):
"""
Clear all items from the cache.
"""
@create_cache_folder @abc.abstractmethod
def load_cache(hash_val): def __contains__(self, key):
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill" """
if cache_path.exists(): Check if the key is in the cache.
with cache_path.open("rb") as cache_file:
return dill.load(cache_file) Args:
return None 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
View 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})"

View file

@ -54,7 +54,7 @@ class CacheManager(Subject):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.CACHE = {} self._cache = {}
self.current_client_id = None self.current_client_id = None
self.current_cache = {} self.current_cache = {}
@ -68,12 +68,12 @@ class CacheManager(Subject):
""" """
previous_client_id = self.current_client_id previous_client_id = self.current_client_id
self.current_client_id = client_id self.current_client_id = client_id
self.current_cache = self.CACHE.setdefault(client_id, {}) self.current_cache = self._cache.setdefault(client_id, {})
try: try:
yield yield
finally: finally:
self.current_client_id = previous_client_id self.current_client_id = previous_client_id
self.current_cache = self.CACHE.get(self.current_client_id, {}) self.current_cache = self._cache.get(self.current_client_id, {})
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None): def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
""" """

134
src/backend/langflow/cache/utils.py vendored Normal file
View 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

View file

@ -10,7 +10,9 @@ from langflow.utils.logger import logger
import asyncio import asyncio
import json import json
from typing import Dict, List from typing import Any, Dict, List
from langflow.cache.flow import InMemoryCache
class ChatHistory(Subject): class ChatHistory(Subject):
@ -46,6 +48,7 @@ class ChatManager:
self.chat_history = ChatHistory() self.chat_history = ChatHistory()
self.cache_manager = cache_manager self.cache_manager = cache_manager
self.cache_manager.attach(self.update) self.cache_manager.attach(self.update)
self.in_memory_cache = InMemoryCache()
def on_chat_history_update(self): def on_chat_history_update(self):
"""Send the last chat message to the client.""" """Send the last chat message to the client."""
@ -99,24 +102,30 @@ class ChatManager:
websocket = self.active_connections[client_id] websocket = self.active_connections[client_id]
await websocket.send_json(message.dict()) await websocket.send_json(message.dict())
async def process_message(self, client_id: str, payload: Dict): async def close_connection(self, client_id: str, code: int, reason: str):
if websocket := self.active_connections[client_id]:
await websocket.close(code=code, reason=reason)
self.disconnect(client_id)
async def process_message(
self, client_id: str, payload: Dict, langchain_object: Any
):
# Process the graph data and chat message # Process the graph data and chat message
chat_message = payload.pop("message", "") chat_message = payload.pop("message", "")
chat_message = ChatMessage(message=chat_message) chat_message = ChatMessage(message=chat_message)
self.chat_history.add_message(client_id, chat_message) self.chat_history.add_message(client_id, chat_message)
graph_data = payload # graph_data = payload
start_resp = ChatResponse(message=None, type="start", intermediate_steps="") start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
await self.send_json(client_id, start_resp) await self.send_json(client_id, start_resp)
is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1 # is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1
# Generate result and thought # Generate result and thought
try: try:
logger.debug("Generating result and thought") logger.debug("Generating result and thought")
result, intermediate_steps = await process_graph( result, intermediate_steps = await process_graph(
graph_data=graph_data, langchain_object=langchain_object,
is_first_message=is_first_message,
chat_message=chat_message, chat_message=chat_message,
websocket=self.active_connections[client_id], websocket=self.active_connections[client_id],
) )
@ -149,6 +158,14 @@ class ChatManager:
await self.send_json(client_id, response) await self.send_json(client_id, response)
self.chat_history.add_message(client_id, response) self.chat_history.add_message(client_id, response)
def set_cache(self, client_id: str, langchain_object: Any) -> bool:
"""
Set the cache for a client.
"""
self.in_memory_cache.set(client_id, langchain_object)
return client_id in self.in_memory_cache
async def handle_websocket(self, client_id: str, websocket: WebSocket): async def handle_websocket(self, client_id: str, websocket: WebSocket):
await self.connect(client_id, websocket) await self.connect(client_id, websocket)
@ -169,22 +186,24 @@ class ChatManager:
continue continue
with self.cache_manager.set_client_id(client_id): with self.cache_manager.set_client_id(client_id):
await self.process_message(client_id, payload) langchain_object = self.in_memory_cache.get(client_id)
await self.process_message(client_id, payload, langchain_object)
except Exception as e: except Exception as e:
# Handle any exceptions that might occur # Handle any exceptions that might occur
logger.exception(e) logger.error(e)
# send a message to the client await self.close_connection(
await self.active_connections[client_id].close( client_id=client_id,
code=status.WS_1011_INTERNAL_ERROR, reason=str(e)[:120] code=status.WS_1011_INTERNAL_ERROR,
reason=str(e)[:120],
) )
self.disconnect(client_id)
finally: finally:
try: try:
connection = self.active_connections.get(client_id) await self.close_connection(
if connection: client_id=client_id,
await connection.close(code=1000, reason="Client disconnected") code=status.WS_1000_NORMAL_CLOSURE,
self.disconnect(client_id) reason="Client disconnected",
)
except Exception as e: except Exception as e:
logger.exception(e) logger.error(e)
self.disconnect(client_id) self.disconnect(client_id)

View file

@ -1,23 +1,15 @@
from fastapi import WebSocket from fastapi import WebSocket
from langflow.api.v1.schemas import ChatMessage from langflow.api.v1.schemas import ChatMessage
from langflow.processing.process import (
load_or_build_langchain_object,
)
from langflow.processing.base import get_result_and_steps from langflow.processing.base import get_result_and_steps
from langflow.interface.utils import try_setting_streaming_options from langflow.interface.utils import try_setting_streaming_options
from langflow.utils.logger import logger from langflow.utils.logger import logger
from typing import Dict
async def process_graph( async def process_graph(
graph_data: Dict, langchain_object,
is_first_message: bool,
chat_message: ChatMessage, chat_message: ChatMessage,
websocket: WebSocket, websocket: WebSocket,
): ):
langchain_object = load_or_build_langchain_object(graph_data, is_first_message)
langchain_object = try_setting_streaming_options(langchain_object, websocket) langchain_object = try_setting_streaming_options(langchain_object, websocket)
logger.debug("Loaded langchain object") logger.debug("Loaded langchain object")

View 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

View 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

View 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

View 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

View file

@ -1,4 +1,4 @@
from typing import Dict, List, Type, Union from typing import Dict, Generator, List, Type, Union
from langflow.graph.edge.base import Edge from langflow.graph.edge.base import Edge
from langflow.graph.graph.constants import VERTEX_TYPE_MAP from langflow.graph.graph.constants import VERTEX_TYPE_MAP
@ -31,7 +31,7 @@ class Graph:
Creates a graph from a payload. Creates a graph from a payload.
Args: Args:
payload (Dict): The payload to create the graph from. payload (Dict): The payload to create the graph from.˜`
Returns: Returns:
Graph: The created graph. Graph: The created graph.
@ -107,6 +107,47 @@ class Graph:
raise ValueError("No root node found") raise ValueError("No root node found")
return root_node.build() return root_node.build()
def topological_sort(self) -> List[Vertex]:
"""
Performs a topological sort of the vertices in the graph.
Returns:
List[Vertex]: A list of vertices in topological order.
Raises:
ValueError: If the graph contains a cycle.
"""
# States: 0 = unvisited, 1 = visiting, 2 = visited
state = {node: 0 for node in self.nodes}
sorted_vertices = []
def dfs(node):
if state[node] == 1:
# We have a cycle
raise ValueError(
"Graph contains a cycle, cannot perform topological sort"
)
if state[node] == 0:
state[node] = 1
for edge in node.edges:
if edge.source == node:
dfs(edge.target)
state[node] = 2
sorted_vertices.append(node)
# Visit each node
for node in self.nodes:
if state[node] == 0:
dfs(node)
return list(reversed(sorted_vertices))
def generator_build(self) -> Generator:
"""Builds each vertex in the graph and yields it."""
sorted_vertices = self.topological_sort()
logger.info("Sorted vertices: %s", sorted_vertices)
yield from sorted_vertices
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]: def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:
"""Returns the neighbors of a node.""" """Returns the neighbors of a node."""
neighbors: Dict[Vertex, int] = {} neighbors: Dict[Vertex, int] = {}

View file

@ -1,4 +1,4 @@
from langflow.cache import base as cache_utils from langflow.cache import utils as cache_utils
from langflow.graph.vertex.constants import DIRECT_TYPES from langflow.graph.vertex.constants import DIRECT_TYPES
from langflow.interface import loading from langflow.interface import loading
from langflow.interface.listing import ALL_TYPES_DICT from langflow.interface.listing import ALL_TYPES_DICT

View file

@ -120,7 +120,7 @@ class CSVAgent(CustomAgentExecutor):
class VectorStoreAgent(CustomAgentExecutor): class VectorStoreAgent(CustomAgentExecutor):
"""Vector Store agent""" """Vector store agent"""
@staticmethod @staticmethod
def function_name(): def function_name():
@ -175,7 +175,7 @@ class SQLAgent(CustomAgentExecutor):
def from_toolkit_and_llm( def from_toolkit_and_llm(
cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any
): ):
"""Construct a sql agent from an LLM and tools.""" """Construct an SQL agent from an LLM and tools."""
db = SQLDatabase.from_uri(database_uri) db = SQLDatabase.from_uri(database_uri)
toolkit = SQLDatabaseToolkit(db=db, llm=llm) toolkit = SQLDatabaseToolkit(db=db, llm=llm)
@ -252,7 +252,11 @@ class VectorStoreRouterAgent(CustomAgentExecutor):
): ):
"""Construct a vector store router agent from an LLM and tools.""" """Construct a vector store router agent from an LLM and tools."""
tools = vectorstoreroutertoolkit.get_tools() tools = (
vectorstoreroutertoolkit
if isinstance(vectorstoreroutertoolkit, list)
else vectorstoreroutertoolkit.get_tools()
)
prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX) prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX)
llm_chain = LLMChain( llm_chain = LLMChain(
llm=llm, llm=llm,
@ -302,7 +306,7 @@ class InitializeAgent(CustomAgentExecutor):
CUSTOM_AGENTS = { CUSTOM_AGENTS = {
"JsonAgent": JsonAgent, "JsonAgent": JsonAgent,
"CSVAgent": CSVAgent, "CSVAgent": CSVAgent,
"initialize_agent": InitializeAgent, "AgentInitializer": InitializeAgent,
"VectorStoreAgent": VectorStoreAgent, "VectorStoreAgent": VectorStoreAgent,
"VectorStoreRouterAgent": VectorStoreRouterAgent, "VectorStoreRouterAgent": VectorStoreRouterAgent,
"SQLAgent": SQLAgent, "SQLAgent": SQLAgent,

View file

@ -20,7 +20,10 @@ class ChainCreator(LangChainTypeCreator):
return ChainFrontendNode return ChainFrontendNode
#! We need to find a better solution for this #! We need to find a better solution for this
from_method_nodes = {"ConversationalRetrievalChain": "from_llm"} from_method_nodes = {
"ConversationalRetrievalChain": "from_llm",
"LLMCheckerChain": "from_llm",
}
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:

View file

@ -1,22 +1,8 @@
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict from langflow.cache.utils import memoize_dict
from langflow.graph import Graph from langflow.graph import Graph
from langflow.utils.logger import logger from langflow.utils.logger import logger
def load_langchain_object(data_graph, is_first_message=False):
"""
Load langchain object from cache if it exists, otherwise build it.
"""
computed_hash = compute_dict_hash(data_graph)
if is_first_message:
langchain_object = build_langchain_object(data_graph)
else:
logger.debug("Loading langchain object from cache")
langchain_object = load_cache(computed_hash)
return computed_hash, langchain_object
@memoize_dict(maxsize=10) @memoize_dict(maxsize=10)
def build_langchain_object_with_caching(data_graph): def build_langchain_object_with_caching(data_graph):
""" """

View file

@ -2,10 +2,12 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from langflow.api import router from langflow.api import router
from langflow.database.base import create_db_and_tables
def create_app(): def create_app():
"""Create the FastAPI app and include the router.""" """Create the FastAPI app and include the router."""
app = FastAPI() app = FastAPI()
origins = [ origins = [
@ -25,6 +27,7 @@ def create_app():
) )
app.include_router(router) app.include_router(router)
app.on_event("startup")(create_db_and_tables)
return app return app

View file

@ -170,3 +170,28 @@ def load_flow_from_json(path: str, build=True):
fix_memory_inputs(langchain_object) fix_memory_inputs(langchain_object)
return langchain_object return langchain_object
return graph return graph
def process_tweaks(graph_data: Dict, tweaks: Dict):
"""This function is used to tweak the graph data using the node id and the tweaks dict"""
# the tweaks dict is a dict of dicts
# the key is the node id and the value is a dict of the tweaks
# the dict of tweaks contains the name of a certain parameter and the value to be tweaked
# We need to process the graph data to add the tweaks
if "data" not in graph_data and "nodes" in graph_data:
nodes = graph_data["nodes"]
else:
nodes = graph_data["data"]["nodes"]
for node in nodes:
node_id = node["id"]
if node_id in tweaks:
node_tweaks = tweaks[node_id]
template_data = node["data"]["node"]["template"]
for tweak_name, tweake_value in node_tweaks.items():
if tweak_name in template_data:
template_data[tweak_name]["value"] = tweake_value
print(
f"Something changed in node {node_id} with tweak {tweak_name} and value {tweake_value}"
)
return graph_data

View file

@ -20,10 +20,13 @@ class Settings(BaseSettings):
textsplitters: List[str] = [] textsplitters: List[str] = []
utilities: List[str] = [] utilities: List[str] = []
dev: bool = False dev: bool = False
database_url: str = "sqlite:///./langflow.db"
remove_api_keys: bool = False
class Config: class Config:
validate_assignment = True validate_assignment = True
extra = "ignore" extra = "ignore"
env_prefix = "LANGFLOW_"
@root_validator(allow_reuse=True) @root_validator(allow_reuse=True)
def validate_lists(cls, values): def validate_lists(cls, values):
@ -46,6 +49,11 @@ class Settings(BaseSettings):
self.utilities = new_settings.utilities or [] self.utilities = new_settings.utilities or []
self.dev = dev self.dev = dev
def update_settings(self, **kwargs):
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
def save_settings_to_yaml(settings: Settings, file_path: str): def save_settings_to_yaml(settings: Settings, file_path: str):
with open(file_path, "w") as f: with open(file_path, "w") as f:

View file

@ -37,7 +37,7 @@ class SQLAgentNode(FrontendNode):
), ),
], ],
) )
description: str = """Construct a sql agent from an LLM and tools.""" description: str = """Construct an SQL agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor"] base_classes: list[str] = ["AgentExecutor"]
def to_dict(self): def to_dict(self):
@ -155,6 +155,7 @@ class CSVAgentNode(FrontendNode):
class InitializeAgentNode(FrontendNode): class InitializeAgentNode(FrontendNode):
name: str = "AgentInitializer" name: str = "AgentInitializer"
display_name: str = "AgentInitializer"
template: Template = Template( template: Template = Template(
type_name="initialize_agent", type_name="initialize_agent",
fields=[ fields=[

View file

@ -33,6 +33,14 @@ class ChainFrontendNode(FrontendNode):
field.show = True field.show = True
field.advanced = True field.advanced = True
# We should think of a way to deal with this later
# if field.field_type == "PromptTemplate":
# field.field_type = "str"
# field.multiline = True
# field.show = True
# field.advanced = False
# field.value = field.value.template
# Separated for possible future changes # Separated for possible future changes
if field.name == "prompt" and field.value is None: if field.name == "prompt" and field.value is None:
field.required = True field.required = True
@ -126,7 +134,7 @@ class TimeTravelGuideChainNode(FrontendNode):
), ),
], ],
) )
description: str = "Time travel guide chain to be used in the flow." description: str = "Time travel guide chain."
base_classes: list[str] = [ base_classes: list[str] = [
"LLMChain", "LLMChain",
"BaseCustomChain", "BaseCustomChain",
@ -197,7 +205,7 @@ class CombineDocsChainNode(FrontendNode):
), ),
], ],
) )
description: str = """Construct a zero shot agent from an LLM and tools.""" description: str = """Load question answering chain."""
base_classes: list[str] = ["BaseCombineDocumentsChain", "function"] base_classes: list[str] = ["BaseCombineDocumentsChain", "function"]
def to_dict(self): def to_dict(self):

View file

@ -2,22 +2,22 @@ from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
class DocumentLoaderFrontNode(FrontendNode): def build_template(
@staticmethod suffixes: list, fileTypes: list, name: str = "file_path"
def build_template( ) -> TemplateField:
suffixes: list, fileTypes: list, name: str = "file_path" """Build a template field for a document loader."""
) -> TemplateField: return TemplateField(
"""Build a template field for a document loader.""" field_type="file",
return TemplateField( required=True,
field_type="file", show=True,
required=True, name=name,
show=True, value="",
name=name, suffixes=suffixes,
value="", fileTypes=fileTypes,
suffixes=suffixes, )
fileTypes=fileTypes,
)
class DocumentLoaderFrontNode(FrontendNode):
file_path_templates = { file_path_templates = {
"AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]), "AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
"CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]), "CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]),

View file

@ -12,6 +12,9 @@ class LLMFrontendNode(FrontendNode):
field.name.title().replace("Openai", "OpenAI").replace("_", " ") field.name.title().replace("Openai", "OpenAI").replace("_", " ")
).replace("Api", "API") ).replace("Api", "API")
if "key" not in field.name.lower() and "token" not in field.name.lower():
field.password = False
@staticmethod @staticmethod
def format_azure_field(field: TemplateField): def format_azure_field(field: TemplateField):
if field.name == "model_name": if field.name == "model_name":
@ -22,7 +25,6 @@ class LLMFrontendNode(FrontendNode):
field.value = "azure" field.value = "azure"
elif field.name == "openai_api_version": elif field.name == "openai_api_version":
field.password = False field.password = False
field.value = "2023-03-15-preview"
@staticmethod @staticmethod
def format_llama_field(field: TemplateField): def format_llama_field(field: TemplateField):
@ -44,7 +46,10 @@ class LLMFrontendNode(FrontendNode):
if field.name in SHOW_FIELDS: if field.name in SHOW_FIELDS:
field.show = True field.show = True
if "api" in field.name and ("key" in field.name or "token" in field.name): if "api" in field.name and (
"key" in field.name
or ("token" in field.name and "tokens" not in field.name)
):
field.password = True field.password = True
field.show = True field.show = True
# Required should be False to support # Required should be False to support

View file

@ -52,7 +52,7 @@ class ToolNode(FrontendNode):
), ),
], ],
) )
description: str = "Tool to be used in the flow." description: str = "Converts a chain, agent or function into a tool."
base_classes: list[str] = ["Tool"] base_classes: list[str] = ["Tool"]
def to_dict(self): def to_dict(self):

View file

@ -5,7 +5,16 @@ OPENAI_MODELS = [
"text-babbage-001", "text-babbage-001",
"text-ada-001", "text-ada-001",
] ]
CHAT_OPENAI_MODELS = ["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"] CHAT_OPENAI_MODELS = [
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k-0613",
"gpt-3.5-turbo-16k",
"gpt-4-0613",
"gpt-4-32k-0613",
"gpt-4",
"gpt-4-32k",
]
ANTHROPIC_MODELS = [ ANTHROPIC_MODELS = [
"claude-v1", # largest model, ideal for a wide range of more complex tasks. "claude-v1", # largest model, ideal for a wide range of more complex tasks.

View file

@ -1,4 +1,5 @@
import ast import ast
import contextlib
import importlib import importlib
import types import types
from typing import Dict from typing import Dict
@ -147,11 +148,8 @@ def create_function(code, function_name):
code_obj = compile( code_obj = compile(
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec" ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
) )
try: with contextlib.suppress(Exception):
exec(code_obj, exec_globals, locals()) exec(code_obj, exec_globals, locals())
except Exception:
pass
exec_globals[function_name] = locals()[function_name] exec_globals[function_name] = locals()[function_name]
# Return a function that imports necessary modules and calls the target function # Return a function that imports necessary modules and calls the target function

View file

@ -0,0 +1 @@
/usr/lib/node_modules/opencommit/out/cli.cjs

File diff suppressed because it is too large Load diff

View file

@ -1,101 +1,106 @@
{ {
"name": "langflow", "name": "langflow",
"version": "0.1.2", "version": "0.1.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@headlessui/react": "^1.7.10", "@headlessui/react": "^1.7.10",
"@heroicons/react": "^2.0.15", "@heroicons/react": "^2.0.15",
"@mui/material": "^5.11.9", "@mui/material": "^5.11.9",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-checkbox": "^1.0.4",
"@tabler/icons-react": "^2.18.0", "@radix-ui/react-dialog": "^1.0.4",
"@tailwindcss/forms": "^0.5.3", "@radix-ui/react-label": "^2.0.2",
"@tailwindcss/line-clamp": "^0.4.4", "@radix-ui/react-switch": "^1.0.3",
"ace-builds": "^1.16.0", "@radix-ui/react-tooltip": "^1.0.6",
"add": "^2.0.6", "@tabler/icons-react": "^2.18.0",
"ansi-to-html": "^0.7.2", "@tailwindcss/forms": "^0.5.3",
"axios": "^1.3.2", "@tailwindcss/line-clamp": "^0.4.4",
"base64-js": "^1.5.1", "ace-builds": "^1.16.0",
"class-variance-authority": "^0.6.0", "add": "^2.0.6",
"clsx": "^1.2.1", "ansi-to-html": "^0.7.2",
"esbuild": "^0.17.18", "axios": "^1.3.2",
"lodash": "^4.17.21", "base64-js": "^1.5.1",
"lucide-react": "^0.233.0", "class-variance-authority": "^0.6.0",
"react": "^18.2.0", "clsx": "^1.2.1",
"react-ace": "^10.1.0", "esbuild": "^0.17.18",
"react-cookie": "^4.1.1", "lodash": "^4.17.21",
"react-dom": "^18.2.0", "lucide-react": "^0.233.0",
"react-error-boundary": "^4.0.2", "react": "^18.2.0",
"react-icons": "^4.8.0", "react-ace": "^10.1.0",
"react-laag": "^2.0.5", "react-cookie": "^4.1.1",
"react-markdown": "^8.0.7", "react-dom": "^18.2.0",
"react-router-dom": "^6.8.1", "react-error-boundary": "^4.0.2",
"react-syntax-highlighter": "^15.5.0", "react-icons": "^4.8.0",
"react-tabs": "^6.0.0", "react-laag": "^2.0.5",
"react-tooltip": "^5.13.1", "react-markdown": "^8.0.7",
"reactflow": "^11.5.5", "react-router-dom": "^6.8.1",
"rehype-mathjax": "^4.0.2", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1", "react-tabs": "^6.0.0",
"remark-math": "^5.1.1", "react-tooltip": "^5.13.1",
"shadcn-ui": "^0.1.3", "reactflow": "^11.5.5",
"switch": "^0.0.0", "rehype-mathjax": "^4.0.2",
"table": "^6.8.1", "remark-gfm": "^3.0.1",
"tailwind-merge": "^1.13.0", "remark-math": "^5.1.1",
"tailwindcss-animate": "^1.0.5", "shadcn-ui": "^0.1.3",
"uuid": "^9.0.0", "short-unique-id": "^4.4.4",
"vite-plugin-svgr": "^3.2.0", "switch": "^0.0.0",
"web-vitals": "^2.1.4" "table": "^6.8.1",
}, "tailwind-merge": "^1.13.0",
"scripts": { "tailwindcss-animate": "^1.0.5",
"dev:docker": "vite --host 0.0.0.0", "uuid": "^9.0.0",
"start": "vite", "vite-plugin-svgr": "^3.2.0",
"build": "vite build", "web-vitals": "^2.1.4"
"serve": "vite preview", },
"format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\"" "scripts": {
}, "dev:docker": "vite --host 0.0.0.0",
"eslintConfig": { "start": "vite",
"extends": [ "build": "vite build",
"react-app", "serve": "vite preview",
"react-app/jest" "format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
] },
}, "eslintConfig": {
"browserslist": { "extends": [
"production": [ "react-app",
">0.2%", "react-app/jest"
"not dead", ]
"not op_mini all" },
], "browserslist": {
"development": [ "production": [
"last 1 chrome version", ">0.2%",
"last 1 firefox version", "not dead",
"last 1 safari version" "not op_mini all"
] ],
}, "development": [
"proxy": "http://127.0.0.1:7860", "last 1 chrome version",
"devDependencies": { "last 1 firefox version",
"@swc/cli": "^0.1.62", "last 1 safari version"
"@swc/core": "^1.3.62", ]
"@tailwindcss/typography": "^0.5.9", },
"@testing-library/jest-dom": "^5.16.5", "proxy": "http://127.0.0.1:7860",
"@testing-library/react": "^13.4.0", "devDependencies": {
"@testing-library/user-event": "^13.5.0", "@swc/cli": "^0.1.62",
"@types/jest": "^27.5.2", "@swc/core": "^1.3.62",
"@types/lodash": "^4.14.194", "@tailwindcss/typography": "^0.5.9",
"@types/node": "^16.18.12", "@testing-library/jest-dom": "^5.16.5",
"@types/react": "^18.0.28", "@testing-library/react": "^13.4.0",
"@types/react-dom": "^18.0.11", "@testing-library/user-event": "^13.5.0",
"@types/uuid": "^9.0.1", "@types/jest": "^27.5.2",
"@vitejs/plugin-react-swc": "^3.0.0", "@types/lodash": "^4.14.194",
"autoprefixer": "^10.4.14", "@types/node": "^16.18.12",
"postcss": "^8.4.23", "@types/react": "^18.0.28",
"tailwindcss": "^3.3.2", "@types/react-dom": "^18.0.11",
"typescript": "^5.0.2", "@types/uuid": "^9.0.1",
"vite": "^4.3.5" "@vitejs/plugin-react-swc": "^3.0.0",
} "autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.2",
"vite": "^4.3.5"
}
} }

View file

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View file

@ -7,14 +7,14 @@ import _ from "lodash";
import ErrorAlert from "./alerts/error"; import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice"; import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success"; import SuccessAlert from "./alerts/success";
import ExtraSidebar from "./components/ExtraSidebarComponent";
import { alertContext } from "./contexts/alertContext"; import { alertContext } from "./contexts/alertContext";
import { locationContext } from "./contexts/locationContext"; import { locationContext } from "./contexts/locationContext";
import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import CrashErrorComponent from "./components/CrashErrorComponent"; import CrashErrorComponent from "./components/CrashErrorComponent";
import { TabsContext } from "./contexts/tabsContext"; import { TabsContext } from "./contexts/tabsContext";
import { getVersion } from "./controllers/API"; import { getVersion } from "./controllers/API";
import Router from "./routes";
import Header from "./components/headerComponent";
export default function App() { export default function App() {
let { setCurrent, setShowSideBar, setIsStackedOpen } = let { setCurrent, setShowSideBar, setIsStackedOpen } =
@ -47,13 +47,6 @@ export default function App() {
}> }>
>([]); >([]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((response) => {
setVersion(response.data.version);
});
}, []);
// Use effect hook to update alertsList when a new alert is added // Use effect hook to update alertsList when a new alert is added
useEffect(() => { useEffect(() => {
// If there is an error alert open with data, add it to the alertsList // If there is an error alert open with data, add it to the alertsList
@ -111,7 +104,6 @@ export default function App() {
return ( return (
//need parent component with width and height //need parent component with width and height
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="flex grow-0 shrink basis-auto"></div>
<ErrorBoundary <ErrorBoundary
onReset={() => { onReset={() => {
window.localStorage.removeItem("tabsData"); window.localStorage.removeItem("tabsData");
@ -121,16 +113,8 @@ export default function App() {
}} }}
FallbackComponent={CrashErrorComponent} FallbackComponent={CrashErrorComponent}
> >
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden"> <Header />
<ExtraSidebar /> <Router />
{/* Main area */}
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
{/* Primary column */}
<div className="w-full h-full">
<TabsManagerComponent></TabsManagerComponent>
</div>
</main>
</div>
</ErrorBoundary> </ErrorBoundary>
<div></div> <div></div>
<div <div
@ -166,14 +150,6 @@ export default function App() {
</div> </div>
))} ))}
</div> </div>
<a
target={"_blank"}
href="https://logspace.ai/"
className="absolute left-7 bottom-2 flex h-6 cursor-pointer flex-col items-center justify-start overflow-hidden rounded-lg bg-gray-800 px-2 text-center font-sans text-xs tracking-wide text-gray-300 transition-all duration-500 ease-in-out hover:h-12 dark:bg-gray-100 dark:text-gray-800"
>
{version && <div className="mt-1">⛓️ LangFlow v{version}</div>}
<div className={version ? "mt-2" : "mt-1"}>Created by Logspace</div>
</a>
</div> </div>
); );
} }

View file

@ -1,14 +1,11 @@
import { Handle, Position, useUpdateNodeInternals } from "reactflow"; import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import Tooltip from "../../../../components/TooltipComponent";
import { import {
classNames, classNames,
groupByFamily, groupByFamily,
isValidConnection, isValidConnection,
toFirstUpperCase,
} from "../../../../utils"; } from "../../../../utils";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import InputComponent from "../../../../components/inputComponent"; import InputComponent from "../../../../components/inputComponent";
import ToggleComponent from "../../../../components/toggleComponent";
import InputListComponent from "../../../../components/inputListComponent"; import InputListComponent from "../../../../components/inputListComponent";
import TextAreaComponent from "../../../../components/textAreaComponent"; import TextAreaComponent from "../../../../components/textAreaComponent";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
@ -24,6 +21,8 @@ import { nodeNames, nodeIcons } from "../../../../utils";
import React from "react"; import React from "react";
import { nodeColors } from "../../../../utils"; import { nodeColors } from "../../../../utils";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import { PopUpContext } from "../../../../contexts/popUpContext";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
export default function ParameterComponent({ export default function ParameterComponent({
left, left,
@ -40,6 +39,9 @@ export default function ParameterComponent({
const refHtml = useRef(null); const refHtml = useRef(null);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const { closePopUp } = useContext(PopUpContext);
const { setTabsState, tabId } = useContext(TabsContext);
useEffect(() => { useEffect(() => {
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) { if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2); setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
@ -54,12 +56,27 @@ export default function ParameterComponent({
const [enabled, setEnabled] = useState( const [enabled, setEnabled] = useState(
data.node.template[name]?.value ?? false data.node.template[name]?.value ?? false
); );
useEffect(() => {}, [closePopUp, data.node.template]);
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
let disabled = let disabled =
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false; reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
const { save } = useContext(TabsContext);
const [myData, setMyData] = useState(useContext(typesContext).data); const [myData, setMyData] = useState(useContext(typesContext).data);
const handleOnNewValue = (newValue: any) => {
data.node.template[name].value = newValue;
// Set state to pending
setTabsState((prev) => {
return {
...prev,
[tabId]: {
isPending: true,
},
};
});
};
useEffect(() => { useEffect(() => {
const groupedObj = groupByFamily(myData, tooltipTitle); const groupedObj = groupByFamily(myData, tooltipTitle);
@ -104,7 +121,7 @@ export default function ParameterComponent({
return ( return (
<div <div
ref={ref} ref={ref}
className="w-full flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800 dark:text-white mt-1 px-5 py-2" className="w-full flex flex-wrap justify-between items-center bg-muted dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
> >
<> <>
<div className={"text-sm truncate w-full " + (left ? "" : "text-end")}> <div className={"text-sm truncate w-full " + (left ? "" : "text-end")}>
@ -125,6 +142,7 @@ export default function ParameterComponent({
delayDuration={0} delayDuration={0}
content={refHtml.current} content={refHtml.current}
side={left ? "left" : "right"} side={left ? "left" : "right"}
open={refHtml?.current?.length > 0}
> >
<Handle <Handle
type={left ? "target" : "source"} type={left ? "target" : "source"}
@ -158,19 +176,13 @@ export default function ParameterComponent({
? [""] ? [""]
: data.node.template[name].value : data.node.template[name].value
} }
onChange={(t: string[]) => { onChange={handleOnNewValue}
data.node.template[name].value = t;
save();
}}
/> />
) : data.node.template[name].multiline ? ( ) : data.node.template[name].multiline ? (
<TextAreaComponent <TextAreaComponent
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={handleOnNewValue}
data.node.template[name].value = t;
save();
}}
/> />
) : ( ) : (
<InputComponent <InputComponent
@ -178,84 +190,72 @@ export default function ParameterComponent({
disableCopyPaste={true} disableCopyPaste={true}
password={data.node.template[name].password ?? false} password={data.node.template[name].password ?? false}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t) => { onChange={handleOnNewValue}
data.node.template[name].value = t;
save();
}}
/> />
)} )}
</div> </div>
) : left === true && type === "bool" ? ( ) : left === true && type === "bool" ? (
<div className="mt-2"> <div className="mt-2">
<ToggleComponent <ToggleShadComponent
disabled={disabled} disabled={disabled}
enabled={enabled} enabled={enabled}
setEnabled={(t) => { setEnabled={(t) => {
data.node.template[name].value = t; handleOnNewValue(t);
setEnabled(t); setEnabled(t);
save();
}} }}
size="large"
/> />
</div> </div>
) : left === true && type === "float" ? ( ) : left === true && type === "float" ? (
<FloatComponent <div className="mt-2 w-full">
disabled={disabled} <FloatComponent
disableCopyPaste={true} disabled={disabled}
value={data.node.template[name].value ?? ""} disableCopyPaste={true}
onChange={(t) => { value={data.node.template[name].value ?? ""}
data.node.template[name].value = t; onChange={handleOnNewValue}
save(); />
}} </div>
/>
) : left === true && ) : left === true &&
type === "str" && type === "str" &&
data.node.template[name].options ? ( data.node.template[name].options ? (
<Dropdown <div className="w-full">
options={data.node.template[name].options} <Dropdown
onSelect={(newValue) => (data.node.template[name].value = newValue)} options={data.node.template[name].options}
value={data.node.template[name].value ?? "Choose an option"} onSelect={handleOnNewValue}
></Dropdown> value={data.node.template[name].value ?? "Choose an option"}
></Dropdown>
</div>
) : left === true && type === "code" ? ( ) : left === true && type === "code" ? (
<CodeAreaComponent <CodeAreaComponent
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={handleOnNewValue}
data.node.template[name].value = t;
save();
}}
/> />
) : left === true && type === "file" ? ( ) : left === true && type === "file" ? (
<InputFileComponent <InputFileComponent
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={handleOnNewValue}
data.node.template[name].value = t;
}}
fileTypes={data.node.template[name].fileTypes} fileTypes={data.node.template[name].fileTypes}
suffixes={data.node.template[name].suffixes} suffixes={data.node.template[name].suffixes}
onFileChange={(t: string) => { onFileChange={(t: string) => {
data.node.template[name].content = t; data.node.template[name].content = t;
save();
}} }}
></InputFileComponent> ></InputFileComponent>
) : left === true && type === "int" ? ( ) : left === true && type === "int" ? (
<IntComponent <div className="mt-2 w-full">
disabled={disabled} <IntComponent
disableCopyPaste={true} disabled={disabled}
value={data.node.template[name].value ?? ""} disableCopyPaste={true}
onChange={(t) => { value={data.node.template[name].value ?? ""}
data.node.template[name].value = t; onChange={handleOnNewValue}
save(); />
}} </div>
/>
) : left === true && type === "prompt" ? ( ) : left === true && type === "prompt" ? (
<PromptAreaComponent <PromptAreaComponent
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={handleOnNewValue}
data.node.template[name].value = t;
save();
}}
/> />
) : ( ) : (
<></> <></>

View file

@ -1,39 +1,18 @@
import { import { classNames, nodeColors, nodeIcons, toTitleCase } from "../../utils";
BugAntIcon,
Cog6ToothIcon,
InformationCircleIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import {
CheckCircleIcon,
EllipsisHorizontalCircleIcon,
ExclamationCircleIcon,
} from "@heroicons/react/24/solid";
import {
classNames,
nodeColors,
nodeIcons,
toNormalCase,
toTitleCase,
} from "../../utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { useContext, useState, useEffect, useRef, Fragment } from "react"; import { useContext, useState, useEffect, useRef } from "react";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import NodeModal from "../../modals/NodeModal"; import NodeModal from "../../modals/NodeModal";
import { useCallback } from "react";
import { TabsContext } from "../../contexts/tabsContext";
import { debounce } from "../../utils";
import Tooltip from "../../components/TooltipComponent"; import Tooltip from "../../components/TooltipComponent";
import { NodeToolbar } from "reactflow"; import { NodeToolbar } from "reactflow";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import { postValidateNode } from "../../controllers/API"; import { useSSE } from "../../contexts/SSEContext";
export default function GenericNode({ export default function GenericNode({
data, data,
selected, selected,
@ -44,46 +23,30 @@ export default function GenericNode({
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const showError = useRef(true); const showError = useRef(true);
const { types, deleteNode } = useContext(typesContext); const { types, deleteNode } = useContext(typesContext);
const { openPopUp } = useContext(PopUpContext);
const { closePopUp, openPopUp } = useContext(PopUpContext);
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]]; const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
const [validationStatus, setValidationStatus] = useState(null); const [validationStatus, setValidationStatus] = useState(null);
// State for outline color // State for outline color
const [isValid, setIsValid] = useState(false); const { sseData, isBuilding } = useSSE();
const { save } = useContext(TabsContext);
const { reactFlowInstance } = useContext(typesContext);
const [params, setParams] = useState([]);
// useEffect(() => {
// if (reactFlowInstance) {
// setParams(Object.values(reactFlowInstance.toObject()));
// }
// }, [save]);
// New useEffect to watch for changes in sseData and update validation status
useEffect(() => { useEffect(() => {
if (reactFlowInstance) { const relevantData = sseData[data.id];
setParams(Object.values(reactFlowInstance.toObject())); if (relevantData) {
// Extract validation information from relevantData and update the validationStatus state
setValidationStatus(relevantData);
} else {
setValidationStatus(null);
} }
}, [save]); }, [sseData, data.id]);
const validateNode = useCallback(
debounce(async () => {
try {
const response = await postValidateNode(
data.id,
reactFlowInstance.toObject()
);
if (response.status === 200) {
let jsonResponseParsed = await JSON.parse(response.data);
setValidationStatus(jsonResponseParsed);
}
} catch (error) {
// console.error("Error validating node:", error);
setValidationStatus("error");
}
}, 1000), // Adjust the debounce delay (500ms) as needed
[reactFlowInstance, data.id]
);
useEffect(() => {
if (params.length > 0) {
validateNode();
}
}, [params, validateNode]);
if (!Icon) { if (!Icon) {
if (showError.current) { if (showError.current) {
@ -98,6 +61,8 @@ export default function GenericNode({
return; return;
} }
useEffect(() => {}, [closePopUp, data.node.template]);
return ( return (
<> <>
<NodeToolbar> <NodeToolbar>
@ -110,11 +75,11 @@ export default function GenericNode({
<div <div
className={classNames( className={classNames(
selected ? "border border-blue-500" : "border dark:border-gray-700", selected ? "border border-ring" : "border dark:border-gray-700",
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900" "prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
)} )}
> >
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-gray-50 p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white "> <div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-muted p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
<div className="flex w-full items-center gap-2 truncate text-lg"> <div className="flex w-full items-center gap-2 truncate text-lg">
<Icon <Icon
className="h-10 w-10 rounded p-1" className="h-10 w-10 rounded p-1"
@ -124,7 +89,7 @@ export default function GenericNode({
/> />
<div className="ml-2 truncate"> <div className="ml-2 truncate">
<ShadTooltip delayDuration={1500} content={data.type}> <ShadTooltip delayDuration={1500} content={data.type}>
<div className="ml-2 truncate">{data.type}</div> <div className="ml-2 truncate text-gray-800">{data.type}</div>
</ShadTooltip> </ShadTooltip>
</div> </div>
</div> </div>
@ -172,7 +137,7 @@ export default function GenericNode({
></div> ></div>
<div <div
className={classNames( className={classNames(
!validationStatus !validationStatus || isBuilding
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100" ? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin", : "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200" "absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
@ -184,8 +149,8 @@ export default function GenericNode({
</div> </div>
</div> </div>
<div className="h-full w-full py-5"> <div className="h-full w-full py-5 text-gray-800">
<div className="w-full px-5 pb-3 text-sm text-gray-500 dark:text-gray-300"> <div className="w-full px-5 pb-3 text-sm text-muted-foreground">
{data.node.description} {data.node.description}
</div> </div>

View file

@ -94,7 +94,7 @@ export default function SingleAlert({
{dropItem.link ? ( {dropItem.link ? (
<Link <Link
to={dropItem.link} to={dropItem.link}
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-blue-600" className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-ring"
> >
Details Details
</Link> </Link>

View file

@ -51,7 +51,7 @@ export default function NoticeAlert({
{link !== "" ? ( {link !== "" ? (
<Link <Link
to={link} to={link}
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-blue-600" className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-ring"
> >
Details Details
</Link> </Link>

View file

@ -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;

View file

@ -21,10 +21,7 @@ export default function ExtraSidebar() {
isStackedOpen ? "w-52" : "w-0 " isStackedOpen ? "w-52" : "w-0 "
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`} } flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
> >
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start"> <div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start bg-white">
<div className="flex px-4 justify-between align-middle w-full">
<span className="text-gray-900 dark:text-white py-[2px] font-medium "></span>
</div>
<div className="flex flex-grow flex-col w-full"> <div className="flex flex-grow flex-col w-full">
{extraNavigation.options ? ( {extraNavigation.options ? (
<div className="p-4"> <div className="p-4">
@ -36,8 +33,8 @@ export default function ExtraSidebar() {
to={item.href} to={item.href}
className={classNames( className={classNames(
item.href.split("/")[2] === current[4] item.href.split("/")[2] === current[4]
? "bg-gray-100 text-gray-900" ? "bg-muted text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900", : "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md" "group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
)} )}
> >
@ -63,9 +60,9 @@ export default function ExtraSidebar() {
<Disclosure.Button <Disclosure.Button
className={classNames( className={classNames(
item.href.split("/")[2] === current[4] item.href.split("/")[2] === current[4]
? "bg-gray-100 text-gray-900" ? "bg-muted text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900", : "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" "group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500"
)} )}
> >
<item.icon <item.icon
@ -96,8 +93,8 @@ export default function ExtraSidebar() {
to={subItem.href} to={subItem.href}
className={classNames( className={classNames(
subItem.href.split("/")[3] === current[5] subItem.href.split("/")[3] === current[5]
? "bg-gray-100 text-gray-900" ? "bg-muted text-gray-900"
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900", : "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium" "group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
)} )}
> >

View file

@ -10,6 +10,7 @@ const ShadTooltip = (props) => {
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={props.delayDuration}> <Tooltip delayDuration={props.delayDuration}>
<TooltipTrigger asChild>{props.children}</TooltipTrigger> <TooltipTrigger asChild>{props.children}</TooltipTrigger>
<TooltipContent <TooltipContent
side={props.side} side={props.side}
avoidCollisions={false} avoidCollisions={false}

View 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">&nbsp;</span>OpenAI+
</Badge> */}
</div>
{button && button}
</div>
</CardFooter>
</Card>
);
};

View 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>
);
}

View file

@ -1,15 +1,23 @@
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { import { MessagesSquare } from "lucide-react";
Bars3CenterLeftIcon,
ChatBubbleBottomCenterTextIcon, import { alertContext } from "../../../contexts/alertContext";
} from "@heroicons/react/24/outline"; import { useContext } from "react";
import { nodeColors } from "../../../utils";
import { PopUpContext } from "../../../contexts/popUpContext"; export default function ChatTrigger({ open, setOpen, isBuilt }) {
import { useContext } from "react"; const { setErrorData } = useContext(alertContext);
import ChatModal from "../../../modals/chatModal";
function handleClick() {
if (isBuilt) {
setOpen(true);
} else {
setErrorData({
title: "Flow not built",
list: ["Please build the flow before chatting"],
});
}
}
export default function ChatTrigger({ open, setOpen }) {
const { openPopUp } = useContext(PopUpContext);
return ( return (
<Transition <Transition
show={!open} show={!open}
@ -23,16 +31,16 @@ export default function ChatTrigger({ open, setOpen }) {
> >
<div className="absolute bottom-4 right-3"> <div className="absolute bottom-4 right-3">
<div <div
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:border-gray-600 cursor-pointer" className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
onClick={() => { bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
setOpen(true); onClick={handleClick}
}}
> >
<button> <button>
<div className="flex gap-3 items-center"> <div className="flex gap-3">
<ChatBubbleBottomCenterTextIcon <MessagesSquare
className="h-6 w-6 mt-1" className="pth-6 w-6 fill-[#5c8be1] stroke-1 stroke-[#5c8be1]"
style={{ color: "white" }} style={{ color: "white" }}
strokeWidth={1.5}
/> />
</div> </div>
</button> </button>

View file

@ -1,18 +1,23 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useNodes } from "reactflow";
import { ChatMessageType, ChatType } from "../../types/chat"; import { ChatType } from "../../types/chat";
import ChatTrigger from "./chatTrigger"; import ChatTrigger from "./chatTrigger";
import BuildTrigger from "./buildTrigger";
import ChatModal from "../../modals/chatModal"; import ChatModal from "../../modals/chatModal";
import _ from "lodash"; import { getBuildStatus } from "../../controllers/API";
import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType) { export default function Chat({ flow }: ChatType) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [isBuilt, setIsBuilt] = useState(false);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if ( if (
(event.key === "K" || event.key === "k") && (event.key === "K" || event.key === "k") &&
(event.metaKey || event.ctrlKey) (event.metaKey || event.ctrlKey) &&
isBuilt
) { ) {
event.preventDefault(); event.preventDefault();
setOpen((oldState) => !oldState); setOpen((oldState) => !oldState);
@ -22,11 +27,58 @@ export default function Chat({ flow }: ChatType) {
return () => { return () => {
document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("keydown", handleKeyDown);
}; };
}, []); }, [isBuilt]);
useEffect(() => {
// Define an async function within the useEffect hook
const fetchBuildStatus = async () => {
const response = await getBuildStatus(flow.id);
setIsBuilt(response.built);
};
// Call the async function
fetchBuildStatus();
}, [flow]);
const prevNodesRef = useRef<any[] | undefined>();
const nodes = useNodes();
useEffect(() => {
const prevNodes = prevNodesRef.current;
const currentNodes = nodes.map(
(node: NodeType) => node.data.node.template.value
);
if (
prevNodes &&
JSON.stringify(prevNodes) !== JSON.stringify(currentNodes)
) {
setIsBuilt(false);
}
prevNodesRef.current = currentNodes;
}, [nodes]);
return ( return (
<> <>
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} /> {isBuilt ? (
<ChatTrigger open={open} setOpen={setOpen} /> <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}
/>
)}
</> </>
); );
} }

View file

@ -4,6 +4,7 @@ import { PopUpContext } from "../../contexts/popUpContext";
import CodeAreaModal from "../../modals/codeAreaModal"; import CodeAreaModal from "../../modals/codeAreaModal";
import TextAreaModal from "../../modals/textAreaModal"; import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import { INPUT_STYLE } from "../../constants";
export default function CodeAreaComponent({ export default function CodeAreaComponent({
value, value,
@ -19,13 +20,18 @@ export default function CodeAreaComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
useEffect(() => {
setMyValue(value);
}, [value]);
return ( return (
<div <div
className={ className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full" disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
} }
> >
<div className="w-full flex items-center gap-3"> <div className="w-full flex items-center">
<span <span
onClick={() => { onClick={() => {
openPopUp( openPopUp(
@ -40,12 +46,14 @@ export default function CodeAreaComponent({
}} }}
className={ className={
editNode editNode
? "h-7 truncate placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "truncate cursor-pointer placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 border-1 shadow-sm sm:text-sm" +
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + INPUT_STYLE
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
INPUT_STYLE +
(disabled ? " bg-gray-200" : "") (disabled ? " bg-gray-200" : "")
} }
> >
{myValue !== "" ? myValue : "Text empty"} {myValue !== "" ? myValue : "Type something..."}
</span> </span>
<button <button
onClick={() => { onClick={() => {
@ -61,7 +69,7 @@ export default function CodeAreaComponent({
}} }}
> >
{!editNode && ( {!editNode && (
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" /> <ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300 ml-3" />
)} )}
</button> </button>
</div> </div>

View file

@ -3,12 +3,14 @@ import { ChevronUpDownIcon, CheckIcon } from "@heroicons/react/24/outline";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { DropDownComponentType } from "../../types/components"; import { DropDownComponentType } from "../../types/components";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { INPUT_STYLE } from "../../constants";
export default function Dropdown({ export default function Dropdown({
value, value,
options, options,
onSelect, onSelect,
editNode = false, editNode = false,
numberOfOptions = 0,
}: DropDownComponentType) { }: DropDownComponentType) {
let [internalValue, setInternalValue] = useState( let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value value === "" || !value ? "Choose an option" : value
@ -25,24 +27,20 @@ export default function Dropdown({
> >
{({ open }) => ( {({ open }) => (
<> <>
<div <div className={editNode ? "mt-1" : "relative mt-1"}>
className={
editNode ? "relative mt-0 w-full" : "relative mt-1 w-full"
}
>
<Listbox.Button <Listbox.Button
className={ className={
editNode editNode
? "pr-9 arrow-hide placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "relative pr-8 placeholder:text-center block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md shadow-sm sm:text-sm border-gray-300 border-1" +
: "relative w-full cursor-default rounded-md border border-gray-300 bg-white dark:bg-gray-900 py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm" INPUT_STYLE
: "ring-1 ring-slate-300 dark:ring-slate-600 w-full py-2 pl-3 pr-10 text-left dark:focus:ring-offset-2 dark:focus:ring-offset-gray-900 dark:focus:ring-1 dark:focus:ring-gray-600 dark:focus-visible:ring-gray-900 dark:focus-visible:ring-offset-2 focus-visible:outline-none dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
INPUT_STYLE
} }
> >
<span className="block truncate w-full">{internalValue}</span> <span className="block truncate w-full">{internalValue}</span>
<span <span
className={ className={
editNode "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
? "hidden"
: "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
} }
> >
<ChevronUpDownIcon <ChevronUpDownIcon
@ -62,8 +60,8 @@ export default function Dropdown({
<Listbox.Options <Listbox.Options
className={ className={
editNode editNode
? "arrow-hide" ? "absolute z-10 mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm w-[215px]"
: "absolute z-50 mt-1 max-h-60 w-full dark:bg-gray-800 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" : "nowheel absolute z-10 mt-1 max-h-60 w-full overflow-auto overflow-y rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm "
} }
> >
{options.map((option, id) => ( {options.map((option, id) => (
@ -71,11 +69,8 @@ export default function Dropdown({
key={id} key={id}
className={({ active }) => className={({ active }) =>
classNames( classNames(
active && !editNode active
? "text-white bg-indigo-600 dark:bg-indigo-500" ? " bg-accent dark:bg-white dark:text-gray-500"
: "text-gray-900",
active && editNode
? "text-white bg-gray-400 dark:bg-gray-500"
: "", : "",
editNode editNode
? "relative cursor-default select-none py-0.5 pl-3 pr-12 dark:text-gray-300 dark:bg-gray-800" ? "relative cursor-default select-none py-0.5 pl-3 pr-12 dark:text-gray-300 dark:bg-gray-800"
@ -89,7 +84,7 @@ export default function Dropdown({
<span <span
className={classNames( className={classNames(
selected ? "font-semibold" : "font-normal", selected ? "font-semibold" : "font-normal",
"block truncate" "block truncate "
)} )}
> >
{option} {option}
@ -98,13 +93,16 @@ export default function Dropdown({
{selected ? ( {selected ? (
<span <span
className={classNames( className={classNames(
editNode ? "text-gray-600" : "text-indigo-600", active ? "text-white dark:text-black" : "",
active ? "text-white" : "",
"absolute inset-y-0 right-0 flex items-center pr-4" "absolute inset-y-0 right-0 flex items-center pr-4"
)} )}
> >
<CheckIcon <CheckIcon
className="h-5 w-5" className={
active
? "h-5 w-5 dark:text-black text-black"
: "h-5 w-5 dark:text-white text-black"
}
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>

View file

@ -1,6 +1,7 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { FloatComponentType } from "../../types/components"; import { FloatComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { INPUT_STYLE } from "../../constants";
export default function FloatComponent({ export default function FloatComponent({
value, value,
@ -12,12 +13,21 @@ export default function FloatComponent({
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const { setDisableCopyPaste } = useContext(TabsContext); const { setDisableCopyPaste } = useContext(TabsContext);
const step = 0.1;
const min = 0;
const max = 1;
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
useEffect(() => {
setMyValue(value);
}, [value]);
return ( return (
<div <div
className={ className={
@ -32,11 +42,24 @@ export default function FloatComponent({
if (disableCopyPaste) setDisableCopyPaste(false); if (disableCopyPaste) setDisableCopyPaste(false);
}} }}
type="number" type="number"
step={step}
min={min}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value < min.toString()) {
e.target.value = min.toString();
}
if (e.target.value > max.toString()) {
e.target.value = max.toString();
}
}}
max={max}
value={myValue} value={myValue}
className={ className={
editNode editNode
? "text-center arrow-hide placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "focus:placeholder-transparent text-center placeholder:text-center border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
: "block w-full form-input dark:bg-gray-900 arrow-hide dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + INPUT_STYLE
: "focus:placeholder-transparent block w-full form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm ring-offset-gray-200 sm:text-sm" +
INPUT_STYLE +
(disabled ? " bg-gray-200 dark:bg-gray-700" : "") (disabled ? " bg-gray-200 dark:bg-gray-700" : "")
} }
placeholder={ placeholder={

View file

@ -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;

View 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>
);
}

View file

@ -2,6 +2,8 @@ import { useContext, useEffect, useState } from "react";
import { InputComponentType } from "../../types/components"; import { InputComponentType } from "../../types/components";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { INPUT_STYLE } from "../../constants";
export default function InputComponent({ export default function InputComponent({
value, value,
@ -14,6 +16,8 @@ export default function InputComponent({
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const { setDisableCopyPaste } = useContext(TabsContext); const { setDisableCopyPaste } = useContext(TabsContext);
const { closePopUp } = useContext(PopUpContext);
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
@ -21,6 +25,10 @@ export default function InputComponent({
} }
}, [disabled, onChange]); }, [disabled, onChange]);
useEffect(() => {
setMyValue(value ?? "");
}, [closePopUp]);
return ( return (
<div <div
className={ className={
@ -38,12 +46,13 @@ export default function InputComponent({
if (disableCopyPaste) setDisableCopyPaste(false); if (disableCopyPaste) setDisableCopyPaste(false);
}} }}
className={classNames( className={classNames(
"block w-full pr-12 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm", "block w-full pr-12 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:placeholder-transparent",
disabled ? " bg-gray-200 dark:bg-gray-700" : "", disabled ? " bg-gray-200 dark:bg-gray-700" : "",
password && !pwdVisible && myValue !== "" ? "password" : "", password && !pwdVisible && myValue !== "" ? "password" : "",
editNode editNode
? "placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200 text-center" ? "border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm text-center" +
: "focus:border-indigo-500 focus:ring-indigo-500", INPUT_STYLE
: "ring-offset-gray-200" + INPUT_STYLE,
password && editNode ? "pr-8" : "pr-3" password && editNode ? "pr-8" : "pr-3"
)} )}
placeholder={password && editNode ? "Key" : "Type something..."} placeholder={password && editNode ? "Key" : "Type something..."}
@ -71,7 +80,11 @@ export default function InputComponent({
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-5 h-5" className={classNames(
editNode
? "w-5 h-5 absolute bottom-0.5 right-2"
: "w-5 h-5 absolute bottom-2 right-3"
)}
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@ -86,7 +99,11 @@ export default function InputComponent({
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-5 h-5" className={classNames(
editNode
? "w-5 h-5 absolute bottom-0.5 right-2"
: "w-5 h-5 absolute bottom-2 right-3"
)}
> >
<path <path
strokeLinecap="round" strokeLinecap="round"

View file

@ -2,6 +2,7 @@ import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { FileComponentType } from "../../types/components"; import { FileComponentType } from "../../types/components";
import { INPUT_STYLE } from "../../constants";
export default function InputFileComponent({ export default function InputFileComponent({
value, value,
@ -37,6 +38,10 @@ export default function InputFileComponent({
return false; return false;
} }
useEffect(() => {
setMyValue(value);
}, [value]);
const handleButtonClick = () => { const handleButtonClick = () => {
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
@ -73,8 +78,10 @@ export default function InputFileComponent({
onClick={handleButtonClick} onClick={handleButtonClick}
className={ className={
editNode editNode
? "placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "truncate placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm border-1" +
: "truncate block w-full text-gray-500 dark:text-gray-300 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + INPUT_STYLE
: "truncate block w-full text-gray-500 dark:text-gray-300 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
INPUT_STYLE +
(disabled ? " bg-gray-200" : "") (disabled ? " bg-gray-200" : "")
} }
> >
@ -82,7 +89,7 @@ export default function InputFileComponent({
</span> </span>
<button onClick={handleButtonClick}> <button onClick={handleButtonClick}>
{!editNode && ( {!editNode && (
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-blue-600" /> <DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-ring" />
)} )}
</button> </button>
</div> </div>

View file

@ -4,11 +4,12 @@ import { InputListComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import _ from "lodash"; import _ from "lodash";
import { INPUT_STYLE } from "../../constants";
export default function InputListComponent({ export default function InputListComponent({
value, value,
onChange, onChange,
disabled, disabled,
editNode = false,
}: InputListComponentType) { }: InputListComponentType) {
const [inputList, setInputList] = useState(value ?? [""]); const [inputList, setInputList] = useState(value ?? [""]);
useEffect(() => { useEffect(() => {
@ -21,7 +22,7 @@ export default function InputListComponent({
<div <div
className={ className={
(disabled ? "pointer-events-none cursor-not-allowed" : "") + (disabled ? "pointer-events-none cursor-not-allowed" : "") +
"flex flex-col gap-3" "flex flex-col gap-3 py-2"
} }
> >
{inputList.map((i, idx) => ( {inputList.map((i, idx) => (
@ -30,8 +31,12 @@ export default function InputListComponent({
type="text" type="text"
value={i} value={i}
className={ className={
"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + editNode
(disabled ? " bg-gray-200" : "") ? "border-[1px] truncate cursor-pointer text-center placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
INPUT_STYLE
: "block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "") +
"focus:placeholder-transparent"
} }
placeholder="Type something..." placeholder="Type something..."
onChange={(e) => { onChange={(e) => {
@ -54,7 +59,7 @@ export default function InputListComponent({
onChange(inputList); onChange(inputList);
}} }}
> >
<PlusIcon className="w-4 h-4 hover:text-blue-600" /> <PlusIcon className={"w-4 h-4 hover:text-ring"} />
</button> </button>
) : ( ) : (
<button <button

View file

@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from "react";
import { FloatComponentType } from "../../types/components"; import { FloatComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { INPUT_STYLE } from "../../constants";
export default function IntComponent({ export default function IntComponent({
value, value,
@ -12,6 +13,7 @@ export default function IntComponent({
}: FloatComponentType) { }: FloatComponentType) {
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const { setDisableCopyPaste } = useContext(TabsContext); const { setDisableCopyPaste } = useContext(TabsContext);
const min = 0;
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -19,6 +21,11 @@ export default function IntComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
useEffect(() => {
setMyValue(value);
}, [value]);
return ( return (
<div <div
className={ className={
@ -53,11 +60,20 @@ export default function IntComponent({
} }
}} }}
type="number" type="number"
step="1"
min={min}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value < min.toString()) {
e.target.value = min.toString();
}
}}
value={myValue} value={myValue}
className={ className={
editNode editNode
? "text-center arrow-hide placeholder:text-center border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "focus:placeholder-transparent text-center placeholder:text-center border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
: "block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 dark:text-gray-300 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + INPUT_STYLE
: "focus:placeholder-transparent block w-full form-input dark:bg-gray-900 dark:border-gray-600 dark:text-gray-300 rounded-md border-gray-300 shadow-sm ring-offset-background sm:text-sm" +
INPUT_STYLE +
(disabled ? " bg-gray-200 dark:bg-gray-700" : "") (disabled ? " bg-gray-200 dark:bg-gray-700" : "")
} }
placeholder={editNode ? "Integer number" : "Type a integer number"} placeholder={editNode ? "Integer number" : "Type a integer number"}

View file

@ -7,7 +7,7 @@ export default function LoadingComponent({ remSize }: LoadingComponentProps) {
<div role="status" className="w-min m-auto"> <div role="status" className="w-min m-auto">
<svg <svg
aria-hidden="true" aria-hidden="true"
className={`w-${remSize} h-${remSize} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`} className={`w-${remSize} h-${remSize} mr-2 text-muted animate-spin dark:text-gray-600 fill-blue-600`}
viewBox="0 0 100 101" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -1,11 +1,10 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import CodeAreaModal from "../../modals/codeAreaModal";
import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { TypeModal } from "../../utils"; import { TypeModal } from "../../utils";
import { INPUT_STYLE } from "../../constants";
export default function PromptAreaComponent({ export default function PromptAreaComponent({
value, value,
@ -21,6 +20,11 @@ export default function PromptAreaComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
useEffect(() => {
setMyValue(value);
}, [value]);
return ( return (
<div <div
className={ className={
@ -45,12 +49,13 @@ export default function PromptAreaComponent({
}} }}
className={ className={
editNode editNode
? "h-7 truncate placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "cursor-pointer truncate placeholder:text-center text-gray-500 border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + INPUT_STYLE
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
(disabled ? " bg-gray-200" : "") (disabled ? " bg-gray-200" : "")
} }
> >
{myValue !== "" ? myValue : "-"} {myValue !== "" ? myValue : "Type your prompt here"}
</span> </span>
<button <button
onClick={() => { onClick={() => {
@ -69,7 +74,7 @@ export default function PromptAreaComponent({
}} }}
> >
{!editNode && ( {!editNode && (
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" /> <ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" />
)} )}
</button> </button>
</div> </div>

View file

@ -4,7 +4,7 @@ import { PopUpContext } from "../../contexts/popUpContext";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { TypeModal } from "../../utils"; import { TypeModal } from "../../utils";
import { INPUT_STYLE } from "../../constants";
export default function TextAreaComponent({ export default function TextAreaComponent({
value, value,
onChange, onChange,
@ -12,16 +12,28 @@ export default function TextAreaComponent({
editNode = false, editNode = false,
}: TextAreaComponentType) { }: TextAreaComponentType) {
const [myValue, setMyValue] = useState(value); const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext); const { openPopUp, closePopUp } = useContext(PopUpContext);
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
useEffect(() => {
setMyValue(value);
}, [closePopUp]);
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div className="w-full flex items-center gap-3"> <div
className={
editNode
? "w-full flex items-center"
: "w-full flex items-center gap-3"
}
>
<span <span
onClick={() => { onClick={() => {
openPopUp( openPopUp(
@ -39,12 +51,13 @@ export default function TextAreaComponent({
}} }}
className={ className={
editNode editNode
? "h-7 truncate placeholder:text-center text-gray-500 border-0 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" ? "truncate cursor-pointer placeholder:text-center text-gray-500 border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
: "truncate block w-full text-gray-500 dark:text-gray-100 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + INPUT_STYLE
: "truncate block w-full text-gray-500 dark:text-muted px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
(disabled ? " bg-gray-200" : "") (disabled ? " bg-gray-200" : "")
} }
> >
{myValue !== "" ? myValue : "Text empty"} {myValue !== "" ? myValue : "Type something..."}
</span> </span>
<button <button
onClick={() => { onClick={() => {
@ -63,7 +76,7 @@ export default function TextAreaComponent({
}} }}
> >
{!editNode && ( {!editNode && (
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" /> <ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" />
)} )}
</button> </button>
</div> </div>

View file

@ -21,8 +21,8 @@ export default function ToggleComponent({
setEnabled(x); setEnabled(x);
}} }}
className={classNames( className={classNames(
enabled ? "bg-indigo-600" : "bg-gray-200", enabled ? "bg-primary" : "bg-gray-200",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-1 focus:ring-primary focus:ring-offset-1"
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>

View file

@ -1,4 +1,3 @@
import { classNames } from "../../utils";
import { useEffect } from "react"; import { useEffect } from "react";
import { ToggleComponentType } from "../../types/components"; import { ToggleComponentType } from "../../types/components";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
@ -7,18 +6,38 @@ export default function ToggleShadComponent({
enabled, enabled,
setEnabled, setEnabled,
disabled, disabled,
size,
}: ToggleComponentType) { }: ToggleComponentType) {
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setEnabled(false); setEnabled(false);
} }
}, [disabled, setEnabled]); }, [disabled, setEnabled]);
let scaleX, scaleY;
switch (size) {
case "small":
scaleX = 0.6;
scaleY = 0.6;
break;
case "medium":
scaleX = 0.8;
scaleY = 0.8;
break;
case "large":
scaleX = 1;
scaleY = 1;
break;
default:
scaleX = 1;
scaleY = 1;
}
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<Switch <Switch
style={{ style={{
transform: "scaleX(0.6) scaleY(0.6)", transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}} }}
className="data-[state=unchecked]:bg-slate-500"
checked={enabled} checked={enabled}
onCheckedChange={(x: boolean) => { onCheckedChange={(x: boolean) => {
setEnabled(x); setEnabled(x);

View 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 };

View file

@ -13,8 +13,10 @@ const buttonVariants = cva(
"bg-destructive text-destructive-foreground hover:bg-destructive/90", "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: outline:
"border border-input hover:bg-accent hover:text-accent-foreground", "border border-input hover:bg-accent hover:text-accent-foreground",
primary:
"border bg-background text-secondary-foreground hover:bg-background/80 hover:shadow-sm",
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary", link: "underline-offset-4 hover:underline text-primary",
}, },

View 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,
};

View file

@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in", "fixed inset-0 z-50 bg-primary/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className className
)} )}
{...props} {...props}

View 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,
};

View 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;

View 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,
};

View 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>
);
}

View 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 };

View 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 };

View file

@ -1,10 +1,19 @@
// src/constants.tsx // src/constants.tsx
import { FlowType } from "./types/flow";
import { buildTweaks } from "./utils";
/** /**
* The base text for subtitle of Export Dialog (Toolbar) * The base text for subtitle of Export Dialog (Toolbar)
* @constant * @constant
*/ */
export const EXPORT_DIALOG_SUBTITLE = "Export your models."; export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
/**
* The base text for subtitle of Flow Settings (Menubar)
* @constant
*/
export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
/** /**
* The base text for subtitle of Code Dialog (Toolbar) * The base text for subtitle of Code Dialog (Toolbar)
@ -18,22 +27,382 @@ export const CODE_DIALOG_SUBTITLE =
* @constant * @constant
*/ */
export const EDIT_DIALOG_SUBTITLE = export const EDIT_DIALOG_SUBTITLE =
"Make configurations changes to your nodes. Click save when you're done."; "Adjust the configurations of your component. Define parameter visibility for the canvas view. Remember to save once you’re finished.";
/** /**
* The base text for subtitle of Code Dialog * The base text for subtitle of Code Dialog
* @constant * @constant
*/ */
export const CODE_PROMPT_DIALOG_SUBTITLE = "Edit you python code."; export const CODE_PROMPT_DIALOG_SUBTITLE =
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
/** /**
* The base text for subtitle of Prompt Dialog * The base text for subtitle of Prompt Dialog
* @constant * @constant
*/ */
export const PROMPT_DIALOG_SUBTITLE = "Edit you prompt."; export const PROMPT_DIALOG_SUBTITLE =
"Create your prompt. Prompts can help guide the behavior of a Language Model.";
/** /**
* The base text for subtitle of Text Dialog * The base text for subtitle of Text Dialog
* @constant * @constant
*/ */
export const TEXT_DIALOG_SUBTITLE = "Edit you text."; export const TEXT_DIALOG_SUBTITLE = "Edit your text.";
/**
* Function to get the python code for the API
* @param {string} flowId - The id of the flow
* @returns {string} - The python code
*/
export const getPythonApiCode = (flow: FlowType): string => {
const flowId = flow.id;
// create a dictionary of node ids and the values is an empty dictionary
// flow.data.nodes.forEach((node) => {
// node.data.id
// }
const tweaks = buildTweaks(flow);
return `import requests
BASE_API_URL = "${window.location.protocol}//${
window.location.host
}/api/v1/predict"
FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaks, null, 2)}
def run_flow(message: str, flow_id: str, tweaks: dict = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {"message": message}
if tweaks:
payload["tweaks"] = tweaks
response = requests.post(api_url, json=payload)
return response.json()
# Setup any tweaks you want to apply to the flow
print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))`;
};
/**
* Function to get the curl code for the API
* @param {string} flowId - The id of the flow
* @returns {string} - The curl code
*/
export const getCurlCode = (flow: FlowType): string => {
const flowId = flow.id;
const tweaks = buildTweaks(flow);
return `curl -X POST \\
${window.location.protocol}//${
window.location.host
}/api/v1/predict/${flowId} \\
-H 'Content-Type: application/json' \\
-d '{"message": "Your message", "tweaks": ${JSON.stringify(
tweaks,
null,
2
)}}'`;
};
/**
* Function to get the python code for the API
* @param {string} flowName - The name of the flow
* @returns {string} - The python code
*/
export const getPythonCode = (flow: FlowType): string => {
const flowName = flow.name;
return `from langflow import load_flow_from_json
flow = load_flow_from_json("${flowName}.json")
# Now you can use it like any chain
flow("Hey, have you heard of LangFlow?")`;
};
/**
* The base text for subtitle of Import Dialog
* @constant
*/
export const IMPORT_DIALOG_SUBTITLE =
"Upload a JSON file or select from the available community examples.";
/**
* The base text for subtitle of code dialog
* @constant
*/
export const EXPORT_CODE_DIALOG =
"Generate the code to integrate your flow into an external application.";
/**
* The base text for subtitle of code dialog
* @constant
*/
export const INPUT_STYLE =
" focus:ring-1 focus:ring-offset-1 focus:ring-ring focus:outline-none ";
/**
* Default description for the flow
* @constant
*/
export const DESCRIPTIONS: string[] = [
"Chain the Words, Master Language!",
"Language Architect at Work!",
"Empowering Language Engineering.",
"Craft Language Connections Here.",
"Create, Connect, Converse.",
"Smart Chains, Smarter Conversations.",
"Bridging Prompts for Brilliance.",
"Language Models, Unleashed.",
"Your Hub for Text Generation.",
"Promptly Ingenious!",
"Building Linguistic Labyrinths.",
"LangFlow: Create, Chain, Communicate.",
"Connect the Dots, Craft Language.",
"Interactive Language Weaving.",
"Generate, Innovate, Communicate.",
"Conversation Catalyst Engine.",
"Language Chainlink Master.",
"Design Dialogues with LangFlow.",
"Nurture NLP Nodes Here.",
"Conversational Cartography Unlocked.",
"Design, Develop, Dialogize.",
];
/**
* Adjectives for the name of the flow
* @constant
*
*/
export const ADJECTIVES: string[] = [
"admiring",
"adoring",
"agitated",
"amazing",
"angry",
"awesome",
"backstabbing",
"berserk",
"big",
"boring",
"clever",
"cocky",
"compassionate",
"condescending",
"cranky",
"desperate",
"determined",
"distracted",
"dreamy",
"drunk",
"ecstatic",
"elated",
"elegant",
"evil",
"fervent",
"focused",
"furious",
"gigantic",
"gloomy",
"goofy",
"grave",
"happy",
"high",
"hopeful",
"hungry",
"insane",
"jolly",
"jovial",
"kickass",
"lonely",
"loving",
"mad",
"modest",
"naughty",
"nauseous",
"nostalgic",
"pedantic",
"pensive",
"prickly",
"reverent",
"romantic",
"sad",
"serene",
"sharp",
"sick",
"silly",
"sleepy",
"small",
"stoic",
"stupefied",
"suspicious",
"tender",
"thirsty",
"tiny",
"trusting",
];
/**
* Nouns for the name of the flow
* @constant
*
*/
export const NOUNS: string[] = [
"albattani",
"allen",
"almeida",
"archimedes",
"ardinghelli",
"aryabhata",
"austin",
"babbage",
"banach",
"bardeen",
"bartik",
"bassi",
"bell",
"bhabha",
"bhaskara",
"blackwell",
"bohr",
"booth",
"borg",
"bose",
"boyd",
"brahmagupta",
"brattain",
"brown",
"carson",
"chandrasekhar",
"colden",
"cori",
"cray",
"curie",
"darwin",
"davinci",
"dijkstra",
"dubinsky",
"easley",
"einstein",
"elion",
"engelbart",
"euclid",
"euler",
"fermat",
"fermi",
"feynman",
"franklin",
"galileo",
"gates",
"goldberg",
"goldstine",
"goldwasser",
"golick",
"goodall",
"hamilton",
"hawking",
"heisenberg",
"heyrovsky",
"hodgkin",
"hoover",
"hopper",
"hugle",
"hypatia",
"jang",
"jennings",
"jepsen",
"joliot",
"jones",
"kalam",
"kare",
"keller",
"khorana",
"kilby",
"kirch",
"knuth",
"kowalevski",
"lalande",
"lamarr",
"leakey",
"leavitt",
"lichterman",
"liskov",
"lovelace",
"lumiere",
"mahavira",
"mayer",
"mccarthy",
"mcclintock",
"mclean",
"mcnulty",
"meitner",
"meninsky",
"mestorf",
"minsky",
"mirzakhani",
"morse",
"murdock",
"newton",
"nobel",
"noether",
"northcutt",
"noyce",
"panini",
"pare",
"pasteur",
"payne",
"perlman",
"pike",
"poincare",
"poitras",
"ptolemy",
"raman",
"ramanujan",
"ride",
"ritchie",
"roentgen",
"rosalind",
"saha",
"sammet",
"shaw",
"shirley",
"shockley",
"sinoussi",
"snyder",
"spence",
"stallman",
"stonebraker",
"swanson",
"swartz",
"swirles",
"tesla",
"thompson",
"torvalds",
"turing",
"varahamihira",
"visvesvaraya",
"volhard",
"wescoff",
"williams",
"wilson",
"wing",
"wozniak",
"wright",
"yalow",
"yonath",
];
/**
* Header text for user projects
* @constant
*
*/
export const USER_PROJECTS_HEADER = "My Collection";

View 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>
);
}

View file

@ -80,7 +80,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
function setErrorData(newState: { title: string; list?: Array<string> }) { function setErrorData(newState: { title: string; list?: Array<string> }) {
setErrorDataState(newState); setErrorDataState(newState);
setErrorOpen(true); setErrorOpen(true);
if (newState.title) { if (newState.title && newState.title !== "") {
setNotificationCenter(true); setNotificationCenter(true);
pushNotificationList({ pushNotificationList({
type: "error", type: "error",
@ -97,7 +97,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
function setNoticeData(newState: { title: string; link?: string }) { function setNoticeData(newState: { title: string; link?: string }) {
setNoticeDataState(newState); setNoticeDataState(newState);
setNoticeOpen(true); setNoticeOpen(true);
if (newState.title) { if (newState.title && newState.title !== "") {
// Add new notice to notification center // Add new notice to notification center
setNotificationCenter(true); setNotificationCenter(true);
pushNotificationList({ pushNotificationList({
@ -117,7 +117,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
setSuccessOpen(true); // open the success alert setSuccessOpen(true); // open the success alert
// If the new state has a "title" property, add a new success notification to the list // If the new state has a "title" property, add a new success notification to the list
if (newState.title) { if (newState.title && newState.title !== "") {
setNotificationCenter(true); // show the notification center setNotificationCenter(true); // show the notification center
pushNotificationList({ pushNotificationList({
// add the new notification to the list // add the new notification to the list

View file

@ -5,22 +5,31 @@ import { LocationProvider } from "./locationContext";
import PopUpProvider from "./popUpContext"; import PopUpProvider from "./popUpContext";
import { TabsProvider } from "./tabsContext"; import { TabsProvider } from "./tabsContext";
import { TypesProvider } from "./typesContext"; import { TypesProvider } from "./typesContext";
import { ReactFlowProvider } from "reactflow";
import { UndoRedoProvider } from "./undoRedoContext";
import { SSEProvider } from "./SSEContext";
export default function ContextWrapper({ children }: { children: ReactNode }) { export default function ContextWrapper({ children }: { children: ReactNode }) {
//element to wrap all context //element to wrap all context
return ( return (
<> <>
<DarkProvider> <ReactFlowProvider>
<TypesProvider> <DarkProvider>
<LocationProvider> <TypesProvider>
<AlertProvider> <LocationProvider>
<TabsProvider> <AlertProvider>
<PopUpProvider>{children}</PopUpProvider> <SSEProvider>
</TabsProvider> <TabsProvider>
</AlertProvider> <UndoRedoProvider>
</LocationProvider> <PopUpProvider>{children}</PopUpProvider>
</TypesProvider> </UndoRedoProvider>
</DarkProvider> </TabsProvider>
</SSEProvider>
</AlertProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</ReactFlowProvider>
</> </>
); );
} }

View file

@ -7,38 +7,52 @@ import {
useContext, useContext,
} from "react"; } from "react";
import { FlowType, NodeType } from "../types/flow"; import { FlowType, NodeType } from "../types/flow";
import { LangFlowState, TabsContextType } from "../types/tabs"; import { TabsContextType, TabsState } from "../types/tabs";
import { import {
normalCaseToSnakeCase,
updateIds, updateIds,
updateObject,
updateTemplate, updateTemplate,
getRandomDescription,
getRandomName,
} from "../utils"; } from "../utils";
import { alertContext } from "./alertContext"; import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext"; import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api"; import { APITemplateType } from "../types/api";
import { v4 as uuidv4 } from "uuid"; import ShortUniqueId from "short-unique-id";
import { addEdge } from "reactflow"; import { addEdge } from "reactflow";
import {
readFlowsFromDatabase,
deleteFlowFromDatabase,
saveFlowToDatabase,
downloadFlowsFromDatabase,
uploadFlowsToDatabase,
updateFlowInDatabase,
} from "../controllers/API";
import _ from "lodash"; import _ from "lodash";
const uid = new ShortUniqueId({ length: 5 });
const TabsContextInitialValue: TabsContextType = { const TabsContextInitialValue: TabsContextType = {
save: () => {}, save: () => {},
tabIndex: 0, tabId: "",
setTabIndex: (index: number) => {}, setTabId: (index: string) => {},
flows: [], flows: [],
removeFlow: (id: string) => {}, removeFlow: (id: string) => {},
addFlow: (flowData?: any) => {}, addFlow: async (flowData?: any) => "",
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uuidv4(), incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
downloadFlows: () => {},
uploadFlows: () => {},
uploadFlow: () => {}, uploadFlow: () => {},
hardReset: () => {}, hardReset: () => {},
saveFlow: async (flow: FlowType) => {},
disableCopyPaste: false, disableCopyPaste: false,
setDisableCopyPaste: (state: boolean) => {}, setDisableCopyPaste: (state: boolean) => {},
lastCopiedSelection: null, lastCopiedSelection: null,
setLastCopiedSelection: (selection: any) => {}, setLastCopiedSelection: (selection: any) => {},
tabsState: {},
getNodeId: () => "", setTabsState: (state: TabsState) => {},
getNodeId: (nodeType: string) => "",
paste: ( paste: (
selection: { nodes: any; edges: any }, selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number }
@ -51,17 +65,21 @@ export const TabsContext = createContext<TabsContextType>(
export function TabsProvider({ children }: { children: ReactNode }) { export function TabsProvider({ children }: { children: ReactNode }) {
const { setErrorData, setNoticeData } = useContext(alertContext); const { setErrorData, setNoticeData } = useContext(alertContext);
const [tabIndex, setTabIndex] = useState(0);
const [tabId, setTabId] = useState("");
const [flows, setFlows] = useState<Array<FlowType>>([]); const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uuidv4()); const [id, setId] = useState(uid());
const { templates, reactFlowInstance } = useContext(typesContext); const { templates, reactFlowInstance } = useContext(typesContext);
const [lastCopiedSelection, setLastCopiedSelection] = useState(null); const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
const [tabsState, setTabsState] = useState<TabsState>({});
const newNodeId = useRef(uuidv4()); const newNodeId = useRef(uid());
function incrementNodeId() { function incrementNodeId() {
newNodeId.current = uuidv4(); newNodeId.current = uid();
return newNodeId.current; return newNodeId.current;
} }
function save() { function save() {
// added clone deep to avoid mutating the original object // added clone deep to avoid mutating the original object
let Saveflows = _.cloneDeep(flows); let Saveflows = _.cloneDeep(flows);
@ -83,71 +101,156 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
window.localStorage.setItem( window.localStorage.setItem(
"tabsData", "tabsData",
JSON.stringify({ tabIndex, flows: Saveflows, id }) JSON.stringify({ tabId, flows: Saveflows, id })
); );
} }
} }
useEffect(() => { // function loadCookie(cookie: string) {
//get tabs locally saved // if (cookie && Object.keys(templates).length > 0) {
let cookie = window.localStorage.getItem("tabsData"); // let cookieObject: LangFlowState = JSON.parse(cookie);
if (cookie && Object.keys(templates).length > 0) { // try {
let cookieObject: LangFlowState = JSON.parse(cookie); // cookieObject.flows.forEach((flow) => {
try { // if (!flow.data) {
cookieObject.flows.forEach((flow) => { // return;
if (!flow.data) { // }
return; // flow.data.edges.forEach((edge) => {
} // edge.className = "";
flow.data.edges.forEach((edge) => { // edge.style = { stroke: "#555555" };
edge.className = ""; // });
edge.style = { stroke: "#555555" };
});
flow.data.nodes.forEach((node) => { // flow.data.nodes.forEach((node) => {
const template = templates[node.data.type]; // const template = templates[node.data.type];
if (!template) { // if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` }); // setErrorData({ title: `Unknown node type: ${node.data.type}` });
return; // return;
} // }
if (Object.keys(template["template"]).length > 0) { // if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"]; // node.data.node.base_classes = template["base_classes"];
flow.data.edges.forEach((edge) => { // flow.data.edges.forEach((edge) => {
if (edge.source === node.id) { // if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle // edge.sourceHandle = edge.sourceHandle
.split("|") // .split("|")
.slice(0, 2) // .slice(0, 2)
.concat(template["base_classes"]) // .concat(template["base_classes"])
.join("|"); // .join("|");
} // }
}); // });
node.data.node.description = template["description"]; // node.data.node.description = template["description"];
node.data.node.template = updateTemplate( // node.data.node.template = updateTemplate(
template["template"] as unknown as APITemplateType, // template["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType // node.data.node.template as APITemplateType
); // );
} // }
}); // });
}); // });
setTabIndex(cookieObject.tabIndex); // setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows); // setFlows(cookieObject.flows);
setId(cookieObject.id); // setId(cookieObject.id);
} catch (e) { // } catch (e) {
console.log(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]); }, [templates]);
useEffect(() => { function getTabsDataFromDB() {
//save tabs locally //get tabs from db
// console.log(id); return readFlowsFromDatabase();
save(); }
}, [flows, id, tabIndex, newNodeId]); 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() { function hardReset() {
newNodeId.current = uuidv4(); newNodeId.current = uid();
setTabIndex(0); setTabId("");
setFlows([]); setFlows([]);
setId(uuidv4()); setId(uid());
} }
/** /**
@ -162,7 +265,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// create a link element and set its properties // create a link element and set its properties
const link = document.createElement("a"); const link = document.createElement("a");
link.href = jsonString; link.href = jsonString;
link.download = `${flows[tabIndex].name}.json`; link.download = `${flows.find((f) => f.id === tabId).name}.json`;
// simulate a click on the link element to trigger the download // simulate a click on the link element to trigger the download
link.click(); link.click();
@ -171,8 +274,24 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
} }
function getNodeId() { function downloadFlows() {
return `dndnode_` + incrementNodeId(); downloadFlowsFromDatabase().then((flows) => {
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flows)
)}`;
// create a link element and set its properties
const link = document.createElement("a");
link.href = jsonString;
link.download = `flows.json`;
// simulate a click on the link element to trigger the download
link.click();
});
}
function getNodeId(nodeType: string) {
return nodeType + "-" + incrementNodeId();
} }
/** /**
@ -180,10 +299,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* If the file type is application/json, the file is read and parsed into a JSON object. * If the file type is application/json, the file is read and parsed into a JSON object.
* The resulting JSON object is passed to the addFlow function. * The resulting JSON object is passed to the addFlow function.
*/ */
function uploadFlow() { function uploadFlow(newProject?: boolean) {
// create a file input // create a file input
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = ".json";
// add a change event listener to the file input // add a change event listener to the file input
input.onchange = (e: Event) => { input.onchange = (e: Event) => {
// check if the file type is application/json // check if the file type is application/json
@ -195,7 +315,29 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// parse the text into a JSON object // parse the text into a JSON object
let flow: FlowType = JSON.parse(text); let flow: FlowType = JSON.parse(text);
addFlow(flow); addFlow(flow, newProject);
});
}
};
// trigger the file input click event to open the file dialog
input.click();
}
function uploadFlows() {
// create a file input
const input = document.createElement("input");
input.type = "file";
// add a change event listener to the file input
input.onchange = (e: Event) => {
// check if the file type is application/json
if ((e.target as HTMLInputElement).files[0].type === "application/json") {
// get the file from the file input
const file = (e.target as HTMLInputElement).files[0];
// read the file as text
const formData = new FormData();
formData.append("file", file);
uploadFlowsToDatabase(formData).then(() => {
refreshFlows();
}); });
} }
}; };
@ -208,21 +350,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* @param {string} id - The id of the flow to remove. * @param {string} id - The id of the flow to remove.
*/ */
function removeFlow(id: string) { function removeFlow(id: string) {
setFlows((prevState) => { const index = flows.findIndex((flow) => flow.id === id);
const newFlows = [...prevState]; if (index >= 0) {
const index = newFlows.findIndex((flow) => flow.id === id); deleteFlowFromDatabase(id).then(() => {
if (index >= 0) { setFlows(flows.filter((flow) => flow.id !== id));
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;
});
} }
/** /**
* Add a new flow to the list of flows. * 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 } ? { x: position.paneX + position.x, y: position.paneY + position.y }
: reactFlowInstance.project({ x: position.x, y: position.y }); : reactFlowInstance.project({ x: position.x, y: position.y });
selectionInstance.nodes.forEach((n) => { selectionInstance.nodes.forEach((n: NodeType) => {
// Generate a unique node ID // Generate a unique node ID
let newId = getNodeId(); let newId = getNodeId(n.data.type);
idsMap[n.id] = newId; idsMap[n.id] = newId;
// Create a new node object // Create a new node object
@ -318,67 +451,106 @@ export function TabsProvider({ children }: { children: ReactNode }) {
reactFlowInstance.setEdges(edges); reactFlowInstance.setEdges(edges);
} }
function addFlow(flow?: FlowType) { const addFlow = async (
// Get data from the flow or set it to null if there's no flow provided. flow?: FlowType,
newProject?: Boolean
): Promise<String> => {
if (newProject) {
let flowData = extractDataFromFlow(flow);
if (flowData.description == "") {
flowData.description = getRandomDescription();
}
// Create a new flow with a default name if no flow is provided.
const newFlow = createNewFlow(flowData, flow);
try {
const { id } = await saveFlowToDatabase(newFlow);
// Change the id to the new id.
newFlow.id = id;
// Add the new flow to the list of flows.
addFlowToLocalState(newFlow);
// Return the id
return id;
} catch (error) {
// Handle the error if needed
console.error("Error while adding flow:", error);
throw error; // Re-throw the error so the caller can handle it if needed
}
} else {
paste(
{ nodes: flow.data.nodes, edges: flow.data.edges },
{ x: 10, y: 10 }
);
}
};
const extractDataFromFlow = (flow) => {
let data = flow?.data ? flow.data : null; let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : ""; const description = flow?.description ? flow.description : "";
if (data) { if (data) {
data.edges.forEach((edge) => { updateEdges(data.edges);
edge.style = { stroke: "inherit" }; updateNodes(data.nodes, data.edges);
edge.className = updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
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);
} }
// 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. return { data, description };
setId(uuidv4()); };
// Add the new flow to the list of flows. const updateEdges = (edges) => {
edges.forEach((edge) => {
setFlows((prevState) => { edge.style = { stroke: "inherit" };
const newFlows = [...prevState, newFlow]; edge.className =
return newFlows; 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 * Updates an existing flow with new data
* @param newFlow - The new flow object containing the updated data * @param newFlow - The new flow object containing the updated data
@ -395,27 +567,64 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return newFlows; return newFlows;
}); });
} }
async function saveFlow(newFlow: FlowType) {
try {
// updates flow in db
const updatedFlow = await updateFlowInDatabase(newFlow);
if (updatedFlow) {
// updates flow in state
setFlows((prevState) => {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
if (index !== -1) {
newFlows[index].description = newFlow.description ?? "";
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
}
return newFlows;
});
//update tabs state
setTabsState((prev) => {
return {
...prev,
[tabId]: {
isPending: false,
},
};
});
}
} catch (err) {
setErrorData(err);
}
}
const [disableCopyPaste, setDisableCopyPaste] = useState(false); const [disableCopyPaste, setDisableCopyPaste] = useState(false);
return ( return (
<TabsContext.Provider <TabsContext.Provider
value={{ value={{
saveFlow,
lastCopiedSelection, lastCopiedSelection,
setLastCopiedSelection, setLastCopiedSelection,
disableCopyPaste, disableCopyPaste,
setDisableCopyPaste, setDisableCopyPaste,
save,
hardReset, hardReset,
tabIndex, tabId,
setTabIndex, setTabId,
flows, flows,
save,
incrementNodeId, incrementNodeId,
removeFlow, removeFlow,
addFlow, addFlow,
updateFlow, updateFlow,
downloadFlow, downloadFlow,
downloadFlows,
uploadFlows,
uploadFlow, uploadFlow,
getNodeId, getNodeId,
tabsState,
setTabsState,
paste, paste,
}} }}
> >

View file

@ -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. // Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes( setTypes(
Object.keys(result.data).reduce((acc, curr) => { // Reverse the keys so the tool world does not overlap
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => { Object.keys(result.data)
acc[c] = curr; .reverse()
// Add the base classes to the accumulator as well. .reduce((acc, curr) => {
result.data[curr][c].base_classes?.forEach((b) => { Object.keys(result.data[curr]).forEach(
acc[b] = curr; (c: keyof APIKindType) => {
}); acc[c] = curr;
}); // Add the base classes to the accumulator as well.
return acc; result.data[curr][c].base_classes?.forEach((b) => {
}, {}) acc[b] = curr;
});
}
);
return acc;
}, {})
); );
} }
// Clear the interval if successful. // Clear the interval if successful.

View file

@ -1,7 +1,19 @@
import { useCallback, useContext, useEffect, useState } from "react"; import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { Edge, Node, useReactFlow } from "reactflow"; import { Edge, Node, useReactFlow } from "reactflow";
import { TabsContext } from "../../../contexts/tabsContext";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { TabsContext } from "./tabsContext";
type undoRedoContextType = {
undo: () => void;
redo: () => void;
takeSnapshot: () => void;
};
type UseUndoRedoOptions = { type UseUndoRedoOptions = {
maxHistorySize: number; maxHistorySize: number;
@ -21,27 +33,34 @@ type HistoryItem = {
edges: Edge[]; edges: Edge[];
}; };
const initialValue = {
undo: () => {},
redo: () => {},
takeSnapshot: () => {},
};
const defaultOptions: UseUndoRedoOptions = { const defaultOptions: UseUndoRedoOptions = {
maxHistorySize: 100, maxHistorySize: 100,
enableShortcuts: true, enableShortcuts: true,
}; };
// https://redux.js.org/usage/implementing-undo-history export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
export const useUndoRedo: UseUndoRedo = ({
maxHistorySize = defaultOptions.maxHistorySize, export function UndoRedoProvider({ children }) {
enableShortcuts = defaultOptions.enableShortcuts, const { tabId, flows } = useContext(TabsContext);
} = defaultOptions) => {
// the past and future arrays store the states that we can jump to
const { tabIndex, flows } = useContext(TabsContext);
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => [])); const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => [])); const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
const [tabIndex, setTabIndex] = useState(
flows.findIndex((f) => f.id === tabId)
);
useEffect(() => { useEffect(() => {
// whenever the flows variable changes, we need to add one array to the past and future states // whenever the flows variable changes, we need to add one array to the past and future states
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : []))); setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : []))); setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
}, [flows]); setTabIndex(flows.findIndex((f) => f.id === tabId));
}, [flows, tabId]);
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow(); const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
@ -50,7 +69,7 @@ export const useUndoRedo: UseUndoRedo = ({
setPast((old) => { setPast((old) => {
let newPast = cloneDeep(old); let newPast = cloneDeep(old);
newPast[tabIndex] = old[tabIndex].slice( newPast[tabIndex] = old[tabIndex].slice(
old[tabIndex].length - maxHistorySize + 1, old[tabIndex].length - defaultOptions.maxHistorySize + 1,
old[tabIndex].length old[tabIndex].length
); );
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() }); newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
@ -63,16 +82,7 @@ export const useUndoRedo: UseUndoRedo = ({
newFuture[tabIndex] = []; newFuture[tabIndex] = [];
return newFuture; return newFuture;
}); });
}, [ }, [getNodes, getEdges, past, future, flows, tabId, setPast, setFuture]);
getNodes,
getEdges,
past,
future,
tabIndex,
setPast,
setFuture,
maxHistorySize,
]);
const undo = useCallback(() => { const undo = useCallback(() => {
// get the last state that we want to go back to // get the last state that we want to go back to
@ -141,7 +151,7 @@ export const useUndoRedo: UseUndoRedo = ({
useEffect(() => { useEffect(() => {
// this effect is used to attach the global event handlers // this effect is used to attach the global event handlers
if (!enableShortcuts) { if (!defaultOptions.enableShortcuts) {
return; return;
} }
@ -165,15 +175,16 @@ export const useUndoRedo: UseUndoRedo = ({
return () => { return () => {
document.removeEventListener("keydown", keyDownHandler); document.removeEventListener("keydown", keyDownHandler);
}; };
}, [undo, redo, enableShortcuts]); }, [undo, redo]);
return (
return { <undoRedoContext.Provider
undo, value={{
redo, undo,
takeSnapshot, redo,
canUndo: !!past.length, takeSnapshot,
canRedo: !!future.length, }}
}; >
}; {children}
</undoRedoContext.Provider>
export default useUndoRedo; );
}

View file

@ -1,19 +1,29 @@
import { PromptTypeAPI, errorsTypeAPI } from "./../../types/api/index"; import {
BuildStatusTypeAPI,
PromptTypeAPI,
errorsTypeAPI,
InitTypeAPI,
} from "./../../types/api/index";
import { APIObjectType, sendAllProps } from "../../types/api/index"; import { APIObjectType, sendAllProps } from "../../types/api/index";
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { FlowType } from "../../types/flow"; import { FlowStyleType, FlowType } from "../../types/flow";
import { ReactFlowJsonObject } from "reactflow";
// when serving with static files
// We need to add /api/v1/ to the url in the axios calls
/** /**
* Retrieves all data from the API. * Fetches all objects from the API endpoint.
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse object containing the API data. *
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
*/ */
export async function getAll(): Promise<AxiosResponse<APIObjectType>> { export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/api/v1/all`); return await axios.get(`/api/v1/all`);
} }
/**
* Sends data to the API for prediction.
*
* @param {sendAllProps} data - The data to be sent to the API.
* @returns {AxiosResponse<any>} The API response.
*/
export async function sendAll(data: sendAllProps) { export async function sendAll(data: sendAllProps) {
return await axios.post(`/api/v1/predict`, data); return await axios.post(`/api/v1/predict`, data);
} }
@ -31,6 +41,12 @@ export async function postValidateNode(
return await axios.post(`/api/v1/validate/node/${nodeId}`, { data }); return await axios.post(`/api/v1/validate/node/${nodeId}`, { data });
} }
/**
* Checks the prompt for the code block by sending it to an API endpoint.
*
* @param {string} template - The template string of the prompt to check.
* @returns {Promise<AxiosResponse<PromptTypeAPI>>} A promise that resolves to an AxiosResponse containing the validation results.
*/
export async function checkPrompt( export async function checkPrompt(
template: string template: string
): Promise<AxiosResponse<PromptTypeAPI>> { ): Promise<AxiosResponse<PromptTypeAPI>> {
@ -38,19 +54,10 @@ export async function checkPrompt(
} }
/** /**
* Retrieves the version of the API. * Fetches a list of JSON files from a GitHub repository and returns their contents as an array of FlowType objects.
* @returns {Promise<AxiosResponse<{ version: string }>>} A promise that resolves to an AxiosResponse object containing the API version. *
* @example * @returns {Promise<FlowType[]>} A promise that resolves to an array of FlowType objects.
* const response = await getVersion();
* console.log(response.data.version);
* // 0.1.0
*/ */
export async function getVersion(): Promise<
AxiosResponse<{ version: string }>
> {
return await axios.get("/api/v1/version");
}
export async function getExamples(): Promise<FlowType[]> { export async function getExamples(): Promise<FlowType[]> {
const url = const url =
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples"; "https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples";
@ -67,3 +74,234 @@ export async function getExamples(): Promise<FlowType[]> {
return await Promise.all(contentsPromises); return await Promise.all(contentsPromises);
} }
/**
* Saves a new flow to the database.
*
* @param {FlowType} newFlow - The flow data to save.
* @returns {Promise<any>} The saved flow data.
* @throws Will throw an error if saving fails.
*/
export async function saveFlowToDatabase(newFlow: {
name: string;
id: string;
data: ReactFlowJsonObject;
description: string;
style?: FlowStyleType;
}): Promise<FlowType> {
try {
const response = await axios.post("/api/v1/flows/", {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Updates an existing flow in the database.
*
* @param {FlowType} updatedFlow - The updated flow data.
* @returns {Promise<any>} The updated flow data.
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType
): Promise<FlowType> {
try {
const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, {
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
});
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Reads all flows from the database.
*
* @returns {Promise<any>} The flows data.
* @throws Will throw an error if reading fails.
*/
export async function readFlowsFromDatabase() {
try {
const response = await axios.get("/api/v1/flows/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
export async function downloadFlowsFromDatabase() {
try {
const response = await axios.get("/api/v1/flows/download/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
export async function uploadFlowsToDatabase(flows) {
try {
const response = await axios.post(`/api/v1/flows/upload/`, flows);
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Deletes a flow from the database.
*
* @param {string} flowId - The ID of the flow to delete.
* @returns {Promise<any>} The deleted flow data.
* @throws Will throw an error if deletion fails.
*/
export async function deleteFlowFromDatabase(flowId: string) {
try {
const response = await axios.delete(`/api/v1/flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches a flow from the database by ID.
*
* @param {number} flowId - The ID of the flow to fetch.
* @returns {Promise<any>} The flow data.
* @throws Will throw an error if fetching fails.
*/
export async function getFlowFromDatabase(flowId: number) {
try {
const response = await axios.get(`/api/v1/flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches flow styles from the database.
*
* @returns {Promise<any>} The flow styles data.
* @throws Will throw an error if fetching fails.
*/
export async function getFlowStylesFromDatabase() {
try {
const response = await axios.get("/api/v1/flow_styles/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Saves a new flow style to the database.
*
* @param {FlowStyleType} flowStyle - The flow style data to save.
* @returns {Promise<any>} The saved flow style data.
* @throws Will throw an error if saving fails.
*/
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
try {
const response = await axios.post("/api/v1/flow_styles/", flowStyle, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches the version of the API.
*
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information.
*/
export async function getVersion() {
const respnose = await axios.get("/api/v1/version");
return respnose.data;
}
/**
* Fetches the health status of the API.
*
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status.
*/
export async function getHealth() {
return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1
}
/**
* Fetches the build status of a flow.
* @param {string} flowId - The ID of the flow to fetch the build status for.
* @returns {Promise<BuildStatusTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
*
*/
export async function getBuildStatus(
flowId: string
): Promise<BuildStatusTypeAPI> {
return await axios.get(`/api/v1/build/${flowId}/status`);
}
//docs for postbuildinit
/**
* Posts the build init of a flow.
* @param {string} flowId - The ID of the flow to fetch the build status for.
* @returns {Promise<InitTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
*
*/
export async function postBuildInit(
flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> {
return await axios.post(`/api/v1/build/init`, flow);
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Before After
Before After

View file

@ -4,115 +4,134 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%; /* hsl(0 0% 100%) */
--foreground: 222.2 47.4% 11.2%; --foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--muted: 210 40% 96.1%; --muted: 210 40% 96.1%; /* hsl(210 40% 96%) */
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
--popover: 0 0% 100%; --popover: 0 0% 100%; /* hsl(0 0% 100%) */
--popover-foreground: 222.2 47.4% 11.2%; --popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--card: 0 0% 100%; --card: 0 0% 100%; /* hsl(0 0% 100%) */
--card-foreground: 222.2 47.4% 11.2%; --card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
--primary: 222.2 47.4% 11.2%; --primary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--primary-foreground: 210 40% 98%; --primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--secondary: 210 40% 96.1%; --secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 40% 96.1%; --accent: 210 40% 96.1%; /* hsl(210 40% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; --destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 215 20.2% 65.1%; --ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: 224 71% 4%; --background: 224 71% 4%; /* hsl(224 71% 4%) */
--foreground: 213 31% 91%; --foreground: 213 31% 91%; /* hsl(213 31% 91%) */
--muted: 223 47% 11%; --muted: 223 47% 11%; /* hsl(223 47% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; --muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
--popover: 224 71% 4%; --popover: 224 71% 4%; /* hsl(224 71% 4%) */
--popover-foreground: 215 20.2% 65.1%; --popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--card: 224 71% 4%; --card: 224 71% 4%; /* hsl(224 71% 4%) */
--card-foreground: 213 31% 91%; --card-foreground: 213 31% 91%; /* hsl(213 31% 91%) */
--border: 216 34% 17%; --border: 216 34% 17%; /* hsl(216 34% 17%) */
--input: 216 34% 17%; --input: 216 34% 17%; /* hsl(216 34% 17%) */
--primary: 210 40% 98%; --primary: 210 40% 98%; /* hsl(210 40% 98%) */
--primary-foreground: 222.2 47.4% 1.2%; --primary-foreground: 222.2 47.4% 1.2%; /* hsl(222 47% 1%) */
--secondary: 222.2 47.4% 11.2%; --secondary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--accent: 216 34% 17%; --accent: 216 34% 17%; /* hsl(216 34% 17%) */
--accent-foreground: 210 40% 98%; --accent-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--destructive: 0 63% 31%; --destructive: 0 63% 31%; /* hsl(0 63% 31%) */
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 216 34% 17%; --ring: 216 34% 17%; /* hsl(216 34% 17%) */
--radius: 0.5rem; --radius: 0.5rem;
} }
} }
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%; /* hsl(0 0% 100%) */
--foreground: 222.2 47.4% 11.2%; --foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--muted: 210 40% 96.1%; --muted: 210 40% 98%; /* hsl(210 40% 98%) */
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
--popover: 0 0% 100%; --popover: 0 0% 100%; /* hsl(0 0% 100%) */
--popover-foreground: 222.2 47.4% 11.2%; --popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--card: 0 0% 100%; --card: 0 0% 100%; /* hsl(0 0% 100%) */
--card-foreground: 222.2 47.4% 11.2%; --card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--border: 214.3 31.8% 91.4%; --border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--input: 214.3 31.8% 91.4%; --input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--primary: 222.2 47.4% 11.2%; --primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
--primary-foreground: 210 40% 98%; --primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--secondary: 210 40% 96.1%; --secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 40% 96.1%; --accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; --destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 215 20.2% 65.1%; --ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
-background: 224 71% 4%; -background: 224 71% 4%;
/* hsl(224 71% 4%) */
-foreground: 213 31% 91%; -foreground: 213 31% 91%;
/* hsl(213 31% 91%) */
-muted: 223 47% 11%; -muted: 223 47% 11%;
/* hsl(223 47% 11%) */
-muted-foreground: 215.4 16.3% 56.9%; -muted-foreground: 215.4 16.3% 56.9%;
/* hsl(215 16% 56%) */
-popover: 224 71% 4%; -popover: 224 71% 4%;
/* hsl(224 71% 4%) */
-popover-foreground: 215 20.2% 65.1%; -popover-foreground: 215 20.2% 65.1%;
/* hsl(215 20% 65%) */
-card: 224 71% 4%; -card: 224 71% 4%;
/* hsl(224 71% 4%) */
-card-foreground: 213 31% 91%; -card-foreground: 213 31% 91%;
/* hsl(213 31% 91%) */
-border: 216 34% 17%; -border: 216 34% 17%;
/* hsl(216 34% 17%) */
-input: 216 34% 17%; -input: 216 34% 17%;
/* hsl(216 34% 17%) */
-primary: 210 40% 98%; -primary: 210 40% 98%;
/* hsl(210 40% 98%) */
-primary-foreground: 222.2 47.4% 1.2%; -primary-foreground: 222.2 47.4% 1.2%;
/* hsl(222 47% 1%) */
-secondary: 222.2 47.4% 11.2%; -secondary: 222.2 47.4% 11.2%;
/* hsl(222 47% 11%) */
-secondary-foreground: 210 40% 98%; -secondary-foreground: 210 40% 98%;
/* hsl(210 40% 98%) */
-accent: 216 34% 17%; -accent: 216 34% 17%;
/* hsl(216 34% 17%) */
-accent-foreground: 210 40% 98%; -accent-foreground: 210 40% 98%;
/* hsl(210 40% 98%) */
-destructive: 0 63% 31%; -destructive: 0 63% 31%;
/* hsl(0 63% 31%) */
-destructive-foreground: 210 40% 98%; -destructive-foreground: 210 40% 98%;
/* hsl(210 40% 98%) */
-ring: 216 34% 17%; -ring: 216 34% 17%;
/* hsl(216 34% 17%) */
-radius: 0.5rem; -radius: 0.5rem;
} }

View file

@ -1,10 +1,5 @@
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react"; import { CodeBracketSquareIcon } from "@heroicons/react/24/outline";
import { import { useContext, useState } from "react";
XMarkIcon,
CommandLineIcon,
CodeBracketSquareIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-github";
@ -18,18 +13,26 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "../../components/ui/dialog"; } from "../../components/ui/dialog";
import { Button } from "../../components/ui/button"; import { FlowType } from "../../types/flow/index";
import { getCurlCode, getPythonApiCode, getPythonCode } from "../../constants";
import { EXPORT_CODE_DIALOG } from "../../constants";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import { Check, Clipboard } from "lucide-react";
export default function ApiModal({ flowName }) { export default function ApiModal({ flow }: { flow: FlowType }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const { dark } = useContext(darkContext); const { dark } = useContext(darkContext);
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState("0");
const [isCopied, setIsCopied] = useState<Boolean>(false); const [isCopied, setIsCopied] = useState<Boolean>(false);
const copyToClipboard = () => { const copyToClipboard = () => {
@ -52,27 +55,18 @@ export default function ApiModal({ flowName }) {
} }
} }
const pythonApiCode = `import requests const pythonApiCode = getPythonApiCode(flow);
import json
API_URL = "${window.location.protocol}//${window.location.host}/predict" const curl_code = getCurlCode(flow);
const pythonCode = getPythonCode(flow);
def predict(message):
with open("${flowName}.json", "r") as f:
json_data = json.load(f)
payload = {'exported_flow': json_data, 'message': message}
response = requests.post(API_URL, json=payload)
return response.json() # JSON {"result": "Response"}
print(predict("Your message"))`;
const pythonCode = `from langflow import load_flow_from_json
flow = load_flow_from_json("${flowName}.json")
# Now you can use it like any chain
flow("Hey, have you heard of LangFlow?")`;
const tabs = [ const tabs = [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
code: curl_code,
},
{ {
name: "Python API", name: "Python API",
mode: "python", mode: "python",
@ -90,7 +84,7 @@ flow("Hey, have you heard of LangFlow?")`;
return ( return (
<Dialog open={true} onOpenChange={setModalOpen}> <Dialog open={true} onOpenChange={setModalOpen}>
<DialogTrigger></DialogTrigger> <DialogTrigger></DialogTrigger>
<DialogContent className="lg:max-w-[800px] sm:max-w-[600px] h-[550px]"> <DialogContent className="lg:max-w-[800px] sm:max-w-[600px] h-[580px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
<span className="pr-2">Code</span> <span className="pr-2">Code</span>
@ -99,57 +93,46 @@ flow("Hey, have you heard of LangFlow?")`;
aria-hidden="true" aria-hidden="true"
/> />
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>{EXPORT_CODE_DIALOG}</DialogDescription>
Export your flow to use it with this code.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col h-full w-full "> <Tabs
<div className="flex px-5 z-10"> defaultValue={"0"}
{tabs.map((tab, index) => ( className="w-full h-full overflow-hidden text-center bg-muted rounded-md border"
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 <button
key={index} className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={() => { onClick={copyToClipboard}
setActiveTab(index);
}}
className={
"p-2 rounded-t-lg w-44 border border-b-0 border-gray-300 dark:border-gray-700 dark:text-gray-300 -mr-px flex justify-center items-center gap-4 " +
(activeTab === index
? " bg-white dark:bg-gray-800"
: "bg-gray-100 dark:bg-gray-900")
}
> >
{tab.name} {isCopied ? <Check size={18} /> : <Clipboard size={15} />}
<img src={tab.image} className="w-6" /> {isCopied ? "Copied!" : "Copy code"}
</button> </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> </div>
<SyntaxHighlighter
className="h-[370px] w-full"
language={tabs[activeTab].mode}
style={oneDark}
customStyle={{ margin: 0 }}
>
{tabs[activeTab].code}
</SyntaxHighlighter>
</div> </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> </DialogContent>
</Dialog> </Dialog>
); );

View file

@ -1,37 +1,27 @@
import { import { useContext, useEffect, useRef, useState } from "react";
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
PencilSquareIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { classNames, limitScrollFieldsModal, nodeIcons } from "../../utils"; import { classNames, limitScrollFieldsModal } from "../../utils";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { Switch } from "../../components/ui/switch";
import ToggleShadComponent from "../../components/toggleShadComponent"; import ToggleShadComponent from "../../components/toggleShadComponent";
import { VariableIcon } from "@heroicons/react/24/outline"; import { VariableIcon } from "@heroicons/react/24/outline";
import InputListComponent from "../../components/inputListComponent"; import InputListComponent from "../../components/inputListComponent";
import TextAreaComponent from "../../components/textAreaComponent"; import TextAreaComponent from "../../components/textAreaComponent";
import InputComponent from "../../components/inputComponent"; import InputComponent from "../../components/inputComponent";
import ToggleComponent from "../../components/toggleComponent";
import FloatComponent from "../../components/floatComponent"; import FloatComponent from "../../components/floatComponent";
import Dropdown from "../../components/dropdownComponent"; import Dropdown from "../../components/dropdownComponent";
import IntComponent from "../../components/intComponent"; import IntComponent from "../../components/intComponent";
import InputFileComponent from "../../components/inputFileComponent"; import InputFileComponent from "../../components/inputFileComponent";
import PromptAreaComponent from "../../components/promptComponent"; import PromptAreaComponent from "../../components/promptComponent";
import CodeAreaComponent from "../../components/codeAreaComponent"; import CodeAreaComponent from "../../components/codeAreaComponent";
import { TabsContext } from "../../contexts/tabsContext";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -42,7 +32,7 @@ import {
DialogTrigger, DialogTrigger,
} from "../../components/ui/dialog"; } from "../../components/ui/dialog";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { EDIT_DIALOG_SUBTITLE } from "../../constants"; import { Badge } from "../../components/ui/badge";
export default function EditNodeModal({ data }: { data: NodeDataType }) { export default function EditNodeModal({ data }: { data: NodeDataType }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@ -60,12 +50,11 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
data.node.template[t].type === "int") data.node.template[t].type === "int")
).length ).length
); );
const [nodeValue, setNodeValue] = useState(true); const [nodeValue, setNodeValue] = useState(null);
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const { types } = useContext(typesContext); const { types } = useContext(typesContext);
const ref = useRef(); const ref = useRef();
const { save } = useContext(TabsContext); const [enabled, setEnabled] = useState(null);
const [enabled, setEnabled] = useState(false);
if (nodeLength == 0) { if (nodeLength == 0) {
closePopUp(); closePopUp();
} }
@ -77,6 +66,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
} }
} }
useEffect(() => {}, [closePopUp, data.node.template]);
function changeAdvanced(node): void { function changeAdvanced(node): void {
Object.keys(data.node.template).filter((n, i) => { Object.keys(data.node.template).filter((n, i) => {
if (n === node.name) { if (n === node.name) {
@ -87,37 +78,32 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
setNodeValue(!nodeValue); setNodeValue(!nodeValue);
} }
// console.log(data.node.template);
return ( return (
<Dialog open={true} onOpenChange={setModalOpen}> <Dialog open={true} onOpenChange={setModalOpen}>
<DialogTrigger></DialogTrigger> <DialogTrigger></DialogTrigger>
<DialogContent className="lg:max-w-[700px]"> <DialogContent className="lg:max-w-[700px] ">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
<span className="pr-2">Edit Node</span> <span className="pr-2">{data.type}</span>
<PencilSquareIcon <Badge variant="secondary">ID: {data.id}</Badge>
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
aria-hidden="true"
/>
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{EDIT_DIALOG_SUBTITLE} {data.node?.description}
<div className="flex pt-3"> <div className="flex pt-4">
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2"> <VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2 dark:text-slate-200">
&nbsp; &nbsp;
</VariableIcon> </VariableIcon>
<span className="text-sm font-semibold text-gray-800"> <span className="text-sm font-semibold text-gray-800 dark:text-white">
Parameters Parameters
</span> </span>
</div> </div>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex w-full h-fit max-h-[415px]"> <div className="flex w-full max-h-[350px] h-fit">
<div <div
className={classNames( className={classNames(
"w-full rounded-lg bg-white dark:bg-gray-800 shadow", "w-full rounded-lg bg-white dark:bg-gray-800 border-[1px] border-gray-200",
nodeLength > limitScrollFieldsModal nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll" ? "overflow-scroll overflow-x-hidden custom-scroll"
: "overflow-hidden" : "overflow-hidden"
@ -125,9 +111,9 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
> >
{nodeLength > 0 && ( {nodeLength > 0 && (
<div className="flex flex-col gap-5 h-fit"> <div className="flex flex-col gap-5 h-fit">
<Table className="table-fixed"> <Table className="table-fixed bg-muted outline-1">
<TableHeader className="border-gray-200 text-gray-500 text-xs font-medium"> <TableHeader className="border-gray-200 text-gray-500 text-xs font-medium h-10">
<TableRow> <TableRow className="dark:border-b-muted">
<TableHead className="h-7 text-center">PARAM</TableHead> <TableHead className="h-7 text-center">PARAM</TableHead>
<TableHead className="p-0 h-7 text-center"> <TableHead className="p-0 h-7 text-center">
VALUE VALUE
@ -150,8 +136,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
data.node.template[t].type === "int") data.node.template[t].type === "int")
) )
.map((n, i) => ( .map((n, i) => (
<TableRow key={i} className="h-8"> <TableRow key={i} className="h-10 dark:border-b-muted">
<TableCell className="p-0 text-center text-gray-900 text-xs dark:text-gray-300"> <TableCell className="p-0 text-center text-gray-900 dark:text-gray-300 text-sm">
{data.node.template[n].name {data.node.template[n].name
? data.node.template[n].name ? data.node.template[n].name
: data.node.template[n].display_name} : data.node.template[n].display_name}
@ -162,6 +148,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
<div className="mx-auto"> <div className="mx-auto">
{data.node.template[n].list ? ( {data.node.template[n].list ? (
<InputListComponent <InputListComponent
editNode={true}
disabled={false} disabled={false}
value={ value={
!data.node.template[n].value || !data.node.template[n].value ||
@ -171,7 +158,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
} }
onChange={(t: string[]) => { onChange={(t: string[]) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
) : data.node.template[n].multiline ? ( ) : data.node.template[n].multiline ? (
@ -181,7 +167,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
value={data.node.template[n].value ?? ""} value={data.node.template[n].value ?? ""}
onChange={(t: string) => { onChange={(t: string) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
) : ( ) : (
@ -194,7 +179,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
value={data.node.template[n].value ?? ""} value={data.node.template[n].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
)} )}
@ -207,8 +191,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
setEnabled={(e) => { setEnabled={(e) => {
data.node.template[n].value = e; data.node.template[n].value = e;
setEnabled(e); setEnabled(e);
save();
}} }}
size="small"
disabled={false} disabled={false}
/> />
</div> </div>
@ -220,7 +204,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
value={data.node.template[n].value ?? ""} value={data.node.template[n].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
</div> </div>
@ -228,6 +211,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
data.node.template[n].options ? ( data.node.template[n].options ? (
<div className="mx-auto"> <div className="mx-auto">
<Dropdown <Dropdown
numberOfOptions={nodeLength}
editNode={true} editNode={true}
options={data.node.template[n].options} options={data.node.template[n].options}
onSelect={(newValue) => onSelect={(newValue) =>
@ -247,7 +231,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
value={data.node.template[n].value ?? ""} value={data.node.template[n].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
</div> </div>
@ -264,7 +247,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
suffixes={data.node.template[n].suffixes} suffixes={data.node.template[n].suffixes}
onFileChange={(t: string) => { onFileChange={(t: string) => {
data.node.template[n].content = t; data.node.template[n].content = t;
save();
}} }}
></InputFileComponent> ></InputFileComponent>
</div> </div>
@ -276,7 +258,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
value={data.node.template[n].value ?? ""} value={data.node.template[n].value ?? ""}
onChange={(t: string) => { onChange={(t: string) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
</div> </div>
@ -288,7 +269,6 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
value={data.node.template[n].value ?? ""} value={data.node.template[n].value ?? ""}
onChange={(t: string) => { onChange={(t: string) => {
data.node.template[n].value = t; data.node.template[n].value = t;
save();
}} }}
/> />
</div> </div>
@ -306,6 +286,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
changeAdvanced(data.node.template[n]) changeAdvanced(data.node.template[n])
} }
disabled={false} disabled={false}
size="small"
/> />
</div> </div>
</TableCell> </TableCell>
@ -326,7 +307,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
}} }}
type="submit" type="submit"
> >
Save changes Save Changes
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View file

@ -21,7 +21,6 @@ export default function ModalField({
type, type,
index, index,
}) { }) {
const { save } = useContext(TabsContext);
const [enabled, setEnabled] = useState( const [enabled, setEnabled] = useState(
data.node.template[name]?.value ?? false data.node.template[name]?.value ?? false
); );
@ -71,7 +70,6 @@ export default function ModalField({
} }
onChange={(t: string[]) => { onChange={(t: string[]) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
) : data.node.template[name].multiline ? ( ) : data.node.template[name].multiline ? (
@ -80,7 +78,6 @@ export default function ModalField({
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={(t: string) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
) : ( ) : (
@ -90,7 +87,6 @@ export default function ModalField({
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
)} )}
@ -104,7 +100,6 @@ export default function ModalField({
setEnabled={(t) => { setEnabled={(t) => {
data.node.template[name].value = t; data.node.template[name].value = t;
setEnabled(t); setEnabled(t);
save();
}} }}
/> />
</div> </div>
@ -115,7 +110,6 @@ export default function ModalField({
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
</div> </div>
@ -134,7 +128,6 @@ export default function ModalField({
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
</div> </div>
@ -150,7 +143,6 @@ export default function ModalField({
suffixes={data.node.template[name].suffixes} suffixes={data.node.template[name].suffixes}
onFileChange={(t: string) => { onFileChange={(t: string) => {
data.node.template[name].content = t; data.node.template[name].content = t;
save();
}} }}
></InputFileComponent> ></InputFileComponent>
</div> </div>
@ -161,7 +153,6 @@ export default function ModalField({
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={(t: string) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
</div> </div>
@ -172,7 +163,6 @@ export default function ModalField({
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t: string) => { onChange={(t: string) => {
data.node.template[name].value = t; data.node.template[name].value = t;
save();
}} }}
/> />
</div> </div>

View file

@ -146,7 +146,7 @@ export default function NodeModal({ data }: { data: NodeDataType }) {
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4"> <div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
<button <button
type="button" type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm" className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:ring-offset-1 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => { onClick={() => {
setModalOpen(false); setModalOpen(false);
}} }}

View file

@ -2,7 +2,7 @@ import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import { TabsContext } from "../../../contexts/tabsContext"; import { TabsContext } from "../../../contexts/tabsContext";
import { INPUT_STYLE } from "../../../constants";
export default function ChatInput({ export default function ChatInput({
lockChat, lockChat,
chatValue, chatValue,
@ -44,9 +44,10 @@ export default function ChatInput({
}} }}
className={classNames( className={classNames(
lockChat lockChat
? " bg-gray-300 text-black dark:bg-gray-700 dark:text-gray-300" ? " bg-input text-black dark:bg-gray-700 dark:text-gray-300"
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300", : " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" "form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" +
INPUT_STYLE
)} )}
placeholder={"Send a message..."} placeholder={"Send a message..."}
/> />

View file

@ -34,8 +34,8 @@ export default function ChatMessage({
className={classNames( className={classNames(
"w-full py-2 pl-2 flex", "w-full py-2 pl-2 flex",
chat.isSend chat.isSend
? "bg-white dark:bg-gray-900 " ? "bg-background dark:bg-gray-900 "
: "bg-gray-200 dark:bg-gray-800" : "bg-input dark:bg-gray-800"
)} )}
> >
<div <div
@ -80,7 +80,7 @@ export default function ChatMessage({
<div <div
onClick={() => setHidden((prev) => !prev)} onClick={() => setHidden((prev) => !prev)}
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500 className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
bg-gray-100 dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll" bg-muted dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: convert.toHtml(chat.thought), __html: convert.toHtml(chat.thought),
}} }}

View file

@ -37,7 +37,7 @@ export default function FileCard({ fileName, content, fileType }) {
/> />
{isHovered && ( {isHovered && (
<div <div
className={`absolute top-0 right-0 bg-gray-100 text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`} className={`absolute top-0 right-0 bg-muted text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
> >
<button <button
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300" className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
@ -54,7 +54,7 @@ export default function FileCard({ fileName, content, fileType }) {
return ( return (
<button <button
onClick={handleDownload} onClick={handleDownload}
className="bg-gray-100 shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300" className="bg-muted shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
> >
<div className="flex gap-2 text-current items-center w-full mr-2"> <div className="flex gap-2 text-current items-center w-full mr-2">
{" "} {" "}

View file

@ -1,21 +1,18 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
ChatBubbleOvalLeftEllipsisIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useEffect, useRef, useState } from "react"; import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { FlowType, NodeType } from "../../types/flow"; import { FlowType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { toNormalCase } from "../../utils"; import { validateNodes } from "../../utils";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import ChatMessage from "./chatMessage"; import ChatMessage from "./chatMessage";
import { FaEraser } from "react-icons/fa"; import { Eraser } from "lucide-react";
import { HiX } from "react-icons/hi"; import { X } from "lucide-react";
import { sendAllProps } from "../../types/api"; import { sendAllProps } from "../../types/api";
import { ChatMessageType, ChatType } from "../../types/chat"; import { ChatMessageType } from "../../types/chat";
import ChatInput from "./chatInput"; import ChatInput from "./chatInput";
import _, { set } from "lodash"; import _ from "lodash";
export default function ChatModal({ export default function ChatModal({
flow, flow,
@ -114,6 +111,17 @@ export default function ChatModal({
} }
} }
function getWebSocketUrl(chatId, isDevelopment = false) {
const isSecureProtocol = window.location.protocol === "https:";
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
const host = isDevelopment ? "localhost:7860" : window.location.host;
const chatEndpoint = `/api/v1/chat/${chatId}`;
return `${
isDevelopment ? "ws" : webSocketProtocol
}://${host}${chatEndpoint}`;
}
function handleWsMessage(data: any) { function handleWsMessage(data: any) {
if (Array.isArray(data)) { if (Array.isArray(data)) {
//set chat history //set chat history
@ -180,12 +188,10 @@ export default function ChatModal({
function connectWS() { function connectWS() {
try { try {
const urlWs = const urlWs = getWebSocketUrl(
id.current,
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? `ws://localhost:7860/api/v1/chat/${id.current}` );
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${
window.location.host
}api/v1/chat/${id.current}`;
const newWs = new WebSocket(urlWs); const newWs = new WebSocket(urlWs);
newWs.onopen = () => { newWs.onopen = () => {
console.log("WebSocket connection established!"); console.log("WebSocket connection established!");
@ -264,54 +270,6 @@ export default function ChatModal({
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" }); if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
}, [chatHistory]); }, [chatHistory]);
function validateNode(n: NodeType): Array<string> {
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
setNoticeData({
title:
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
});
return [];
}
const {
type,
node: { template },
} = n.data;
return Object.keys(template).reduce(
(errors: Array<string>, t) =>
errors.concat(
template[t].required &&
template[t].show &&
(template[t].value === undefined ||
template[t].value === null ||
template[t].value === "") &&
!reactFlowInstance
.getEdges()
.some(
(e) =>
e.targetHandle.split("|")[1] === t &&
e.targetHandle.split("|")[2] === n.id
)
? [
`${type} is missing ${
template.display_name
? template.display_name
: toNormalCase(template[t].name)
}.`,
]
: []
),
[] as string[]
);
}
function validateNodes() {
return reactFlowInstance
.getNodes()
.flatMap((n: NodeType) => validateNode(n));
}
const ref = useRef(null); const ref = useRef(null);
useEffect(() => { useEffect(() => {
@ -322,7 +280,7 @@ export default function ChatModal({
function sendMessage() { function sendMessage() {
if (chatValue !== "") { if (chatValue !== "") {
let nodeValidationErrors = validateNodes(); let nodeValidationErrors = validateNodes(reactFlowInstance);
if (nodeValidationErrors.length === 0) { if (nodeValidationErrors.length === 0) {
setLockChat(true); setLockChat(true);
let message = chatValue; let message = chatValue;
@ -394,13 +352,13 @@ export default function ChatModal({
onClick={() => clearChat()} onClick={() => clearChat()}
className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30" className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
> >
<FaEraser className="w-4 h-4" /> <Eraser className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setModalOpen(false)} onClick={() => setModalOpen(false)}
className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30" className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
> >
<HiX className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
<div <div
@ -425,7 +383,7 @@ export default function ChatModal({
</span> </span>
</span> </span>
<br /> <br />
<div className="bg-gray-100 dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700"> <div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
<span className="text-base text-gray-500"> <span className="text-base text-gray-500">
Start a conversation and click the agent’s thoughts{" "} Start a conversation and click the agent’s thoughts{" "}
<span> <span>

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