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
.testenv/*
langflow.db

2
.vscode/launch.json vendored
View file

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

629
poetry.lock generated

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"
typer = "^0.7.0"
gunicorn = "^20.1.0"
langchain = "^0.0.194"
openai = "^0.27.7"
langchain = "^0.0.200"
openai = "^0.27.8"
types-pyyaml = "^6.0.12.8"
dill = "^0.3.6"
pandas = "^1.5.3"
chromadb = "^0.3.21"
huggingface-hub = "^0.13.3"
@ -57,9 +56,13 @@ jina = "3.15.2"
sentence-transformers = "^2.2.2"
ctransformers = "^0.2.2"
cohere = "^4.6.0"
sqlmodel = "^0.0.8"
faiss-cpu = "^1.7.4"
anthropic = "^0.2.9"
orjson = "^3.9.0"
multiprocess = "^0.70.14"
cachetools = "^5.3.1"
types-cachetools = "^5.3.0.5"
[tool.poetry.group.dev.dependencies]

View file

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

View file

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

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.validate import router as validate_router
from langflow.api.v1.chat import router as chat_router
from langflow.api.v1.flows import router as flows_router
from langflow.api.v1.flow_styles import router as flow_styles_router
__all__ = ["chat_router", "endpoints_router", "validate_router"]
__all__ = [
"chat_router",
"endpoints_router",
"validate_router",
"flows_router",
"flow_styles_router",
]

View file

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

View file

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

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

View file

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

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

146
src/backend/langflow/cache/flow.py vendored Normal file
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):
super().__init__()
self.CACHE = {}
self._cache = {}
self.current_client_id = None
self.current_cache = {}
@ -68,12 +68,12 @@ class CacheManager(Subject):
"""
previous_client_id = self.current_client_id
self.current_client_id = client_id
self.current_cache = self.CACHE.setdefault(client_id, {})
self.current_cache = self._cache.setdefault(client_id, {})
try:
yield
finally:
self.current_client_id = previous_client_id
self.current_cache = self.CACHE.get(self.current_client_id, {})
self.current_cache = self._cache.get(self.current_client_id, {})
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
"""

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

View file

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

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

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.interface import loading
from langflow.interface.listing import ALL_TYPES_DICT

View file

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

View file

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

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.utils.logger import logger
def load_langchain_object(data_graph, is_first_message=False):
"""
Load langchain object from cache if it exists, otherwise build it.
"""
computed_hash = compute_dict_hash(data_graph)
if is_first_message:
langchain_object = build_langchain_object(data_graph)
else:
logger.debug("Loading langchain object from cache")
langchain_object = load_cache(computed_hash)
return computed_hash, langchain_object
@memoize_dict(maxsize=10)
def build_langchain_object_with_caching(data_graph):
"""

View file

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

View file

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

View file

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

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"]
def to_dict(self):
@ -155,6 +155,7 @@ class CSVAgentNode(FrontendNode):
class InitializeAgentNode(FrontendNode):
name: str = "AgentInitializer"
display_name: str = "AgentInitializer"
template: Template = Template(
type_name="initialize_agent",
fields=[

View file

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

View file

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

View file

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

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"]
def to_dict(self):

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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",
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
primary:
"border bg-background text-secondary-foreground hover:bg-background/80 hover:shadow-sm",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},

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
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
"fixed inset-0 z-50 bg-primary/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className
)}
{...props}

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

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

View file

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

View file

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

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

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

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Before After
Before After

View file

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

View file

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

View file

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

View file

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

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">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:ring-offset-1 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
setModalOpen(false);
}}

View file

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

View file

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

View file

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

View file

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

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