Adds better prompt experience and many other improvements (#621)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-07-11 18:35:56 -03:00 committed by GitHub
commit ee55d729e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
177 changed files with 4904 additions and 2981 deletions

0
.githooks/pre-commit Normal file → Executable file
View file

3
.gitignore vendored
View file

@ -1,3 +1,5 @@
# This is to avoid Opencommit hook from getting pushed
prepare-commit-msg
# Logs
logs
*.log
@ -242,4 +244,5 @@ dmypy.json
# Poetry
.testenv/*
langflow.db
.githooks/prepare-commit-msg
langchain.db

View file

@ -5,6 +5,8 @@ all: help
init:
@echo 'Installing pre-commit hooks'
git config core.hooksPath .githooks
@echo 'Making pre-commit hook executable'
chmod +x .githooks/pre-commit
@echo 'Installing backend dependencies'
make install_backend
@echo 'Installing frontend dependencies'

View file

@ -31,12 +31,13 @@
- [Table of Contents](#table-of-contents)
- [📦 Installation](#-installation)
- [Locally](#locally)
- [HuggingFace Spaces](#huggingface-spaces)
- [🖥️ Command Line Interface (CLI)](#-command-line-interface-cli)
- [Usage](#usage)
- [Environment Variables](#environment-variables)
- [Deployment](#deployment)
- [Deploy Langflow on Google Cloud Platform](#deploy-langflow-on-google-cloud-platform)
- [Deploy Langflow on Jina AI Cloud](#deploy-langflow-on-jina-ai-cloud)
- [Deploy Langflow on Google Cloud Platform](#deploy-langflow-on-google-cloud-platform)
- [Deploy Langflow on Jina AI Cloud](#deploy-langflow-on-jina-ai-cloud)
- [API Usage](#api-usage)
- [🎨 Creating Flows](#-creating-flows)
- [👋 Contributing](#-contributing)
@ -61,6 +62,8 @@ or
langflow # or langflow --help
```
### HuggingFace Spaces
You can also check it out on [HuggingFace Spaces](https://huggingface.co/spaces/Logspace/Langflow) and run it in your browser! You can even clone it and have your own copy of Langflow to play with.
# 🖥️ Command Line Interface (CLI)
@ -103,7 +106,7 @@ A sample `.env` file named `.env.example` is included with the project. Copy thi
# Deployment
### Deploy Langflow on Google Cloud Platform
## Deploy Langflow on Google Cloud Platform
Follow our step-by-step guide to deploy Langflow on Google Cloud Platform (GCP) using Google Cloud Shell. The guide is available in the [**Langflow in Google Cloud Platform**](GCP_DEPLOYMENT.md) document.
@ -112,7 +115,7 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/logspace-ai/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md)
### Deploy Langflow on [Jina AI Cloud](https://github.com/jina-ai/langchain-serve)
## Deploy Langflow on [Jina AI Cloud](https://github.com/jina-ai/langchain-serve)
Langflow integrates with langchain-serve to provide a one-command deployment to Jina AI Cloud.
@ -219,6 +222,13 @@ print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))
> Read more about resource customization, cost, and management of Langflow apps on Jina AI Cloud in the **[langchain-serve](https://github.com/jina-ai/langchain-serve)** repository.
## Deploy on Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/Emy2sU?referralCode=MnPSdg)
## Deploy on Render
<a href="https://render.com/deploy?repo=https://github.com/logspace-ai/langflow/tree/main">
<img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" />
</a>
# 🎨 Creating Flows

771
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -23,14 +23,14 @@ langflow = "langflow.__main__:main"
[tool.poetry.dependencies]
python = ">=3.9,<3.11"
fastapi = "^0.99.0"
fastapi = "^0.100.0"
uvicorn = "^0.22.0"
beautifulsoup4 = "^4.12.2"
google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0"
typer = "^0.9.0"
gunicorn = "^20.1.0"
langchain = "^0.0.219"
langchain = "^0.0.229"
openai = "^0.27.8"
pandas = "^2.0.0"
chromadb = "^0.3.21"
@ -78,7 +78,7 @@ black = "^23.1.0"
ipykernel = "^6.21.2"
mypy = "^1.1.1"
ruff = "^0.0.254"
httpx = "^0.23.3"
httpx = "*"
pytest = "^7.2.2"
types-requests = "^2.28.11"
requests = "^2.28.0"

11
render.yaml Normal file
View file

@ -0,0 +1,11 @@
services:
# A Docker web service
- type: web
name: langflow
runtime: docker
plan: free
dockerfilePath: ./Dockerfile
repo: https://github.com/logspace-ai/langflow
branch: main
healthCheckPath: /health
autoDeploy: false

View file

@ -22,3 +22,38 @@ def remove_api_keys(flow: dict):
value["value"] = None
return flow
def build_input_keys_response(langchain_object, artifacts):
"""Build the input keys response."""
input_keys_response = {
"input_keys": {key: "" for key in langchain_object.input_keys},
"memory_keys": [],
"handle_keys": artifacts.get("handle_keys", []),
}
# Set the input keys values from artifacts
for key, value in artifacts.items():
if key in input_keys_response["input_keys"]:
input_keys_response["input_keys"][key] = value
# If the object has memory, that memory will have a memory_variables attribute
# memory variables should be removed from the input keys
if hasattr(langchain_object, "memory") and hasattr(
langchain_object.memory, "memory_variables"
):
# Remove memory variables from input keys
input_keys_response["input_keys"] = {
key: value
for key, value in input_keys_response["input_keys"].items()
if key not in langchain_object.memory.memory_variables
}
# Add memory variables to memory_keys
input_keys_response["memory_keys"] = langchain_object.memory.memory_variables
if hasattr(langchain_object, "prompt") and hasattr(
langchain_object.prompt, "template"
):
input_keys_response["template"] = langchain_object.prompt.template
return input_keys_response

View file

@ -1,6 +1,8 @@
from langflow.template.frontend_node.base import FrontendNode
from pydantic import BaseModel, validator
from langflow.interface.utils import extract_input_variables_from_prompt
from langchain.prompts import PromptTemplate
class CacheResponse(BaseModel):
@ -11,8 +13,14 @@ class Code(BaseModel):
code: str
class Prompt(BaseModel):
class FrontendNodeRequest(FrontendNode):
template: dict # type: ignore
class ValidatePromptRequest(BaseModel):
name: str
template: str
frontend_node: FrontendNodeRequest
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
@ -31,6 +39,7 @@ class CodeValidationResponse(BaseModel):
class PromptValidationResponse(BaseModel):
input_variables: list
frontend_node: FrontendNodeRequest
INVALID_CHARACTERS = {
@ -51,34 +60,93 @@ INVALID_CHARACTERS = {
"}",
}
INVALID_NAMES = {
"input_variables",
"output_parser",
"partial_variables",
"template",
"template_format",
"validate_template",
}
def validate_prompt(template: str):
input_variables = extract_input_variables_from_prompt(template)
# Check if there are invalid characters in the input_variables
input_variables = check_input_variables(input_variables)
if any(var in INVALID_NAMES for var in input_variables):
raise ValueError(
f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. "
)
return PromptValidationResponse(input_variables=input_variables)
try:
PromptTemplate(template=template, input_variables=input_variables)
except Exception as exc:
raise ValueError(str(exc)) from exc
return input_variables
def check_input_variables(input_variables: list):
invalid_chars = []
fixed_variables = []
wrong_variables = []
empty_variables = []
for variable in input_variables:
new_var = variable
for char in INVALID_CHARACTERS:
if char in variable:
invalid_chars.append(char)
new_var = new_var.replace(char, "")
# if variable is empty, then we should add that to the wrong variables
if not variable:
empty_variables.append(variable)
continue
# if variable starts with a number we should add that to the invalid chars
# and wrong variables
if variable[0].isdigit():
invalid_chars.append(variable[0])
new_var = new_var.replace(variable[0], "")
wrong_variables.append(variable)
else:
for char in INVALID_CHARACTERS:
if char in variable:
invalid_chars.append(char)
new_var = new_var.replace(char, "")
wrong_variables.append(variable)
fixed_variables.append(new_var)
if new_var != variable:
input_variables.remove(variable)
input_variables.append(new_var)
# If any of the input_variables is not in the fixed_variables, then it means that
# there are invalid characters in the input_variables
if any(var not in fixed_variables for var in input_variables):
raise ValueError(
f"Invalid input variables: {input_variables}. Please, use something like {fixed_variables} instead."
)
if any(var not in fixed_variables for var in input_variables):
error_message = build_error_message(
input_variables,
invalid_chars,
wrong_variables,
fixed_variables,
empty_variables,
)
raise ValueError(error_message)
return input_variables
def build_error_message(
input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables
):
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
error_string = f"Invalid input variables: {input_variables_str}. "
if wrong_variables and invalid_chars:
# fix the wrong variables replacing invalid chars and find them in the fixed variables
error_string_vars = "You can fix them by replacing the invalid characters: "
wvars = wrong_variables.copy()
for i, wrong_var in enumerate(wvars):
for char in invalid_chars:
wrong_var = wrong_var.replace(char, "")
if wrong_var in fixed_variables:
error_string_vars += f"'{wrong_variables[i]}' -> '{wrong_var}'"
error_string += error_string_vars
elif empty_variables:
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
elif len(set(fixed_variables)) != len(fixed_variables):
error_string += "There are duplicate variables."
return error_string

View file

@ -1,22 +1,132 @@
import asyncio
from typing import Any
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
from langflow.api.v1.schemas import ChatResponse
from typing import Any, Dict, List, Union
from fastapi import WebSocket
from langchain.schema import AgentAction, LLMResult, AgentFinish
from langflow.utils.logger import logger
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
"""Callback handler for streaming LLM responses."""
def __init__(self, websocket):
def __init__(self, websocket: WebSocket):
self.websocket = websocket
async def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
await self.websocket.send_json(resp.dict())
async def on_llm_start(
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
) -> Any:
"""Run when LLM starts running."""
async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
"""Run when LLM ends running."""
async def on_llm_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when LLM errors."""
async def on_chain_start(
self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
) -> Any:
"""Run when chain starts running."""
async def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
"""Run when chain ends running."""
async def on_chain_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when chain errors."""
async def on_tool_start(
self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
) -> Any:
"""Run when tool starts running."""
resp = ChatResponse(
message="",
type="stream",
intermediate_steps=f"Tool input: {input_str}",
)
await self.websocket.send_json(resp.dict())
async def on_tool_end(self, output: str, **kwargs: Any) -> Any:
"""Run when tool ends running."""
observation_prefix = kwargs.get("observation_prefix", "Tool output: ")
split_output = output.split()
first_word = split_output[0]
rest_of_output = split_output[1:]
# Create a formatted message.
intermediate_steps = f"{observation_prefix}{first_word}"
# Create a ChatResponse instance.
resp = ChatResponse(
message="",
type="stream",
intermediate_steps=intermediate_steps,
)
rest_of_resps = [
ChatResponse(
message="",
type="stream",
intermediate_steps=f"{word}",
)
for word in rest_of_output
]
resps = [resp] + rest_of_resps
# Try to send the response, handle potential errors.
try:
# This is to emulate the stream of tokens
for resp in resps:
await self.websocket.send_json(resp.dict())
except Exception as e:
logger.error(e)
async def on_tool_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when tool errors."""
async def on_text(self, text: str, **kwargs: Any) -> Any:
"""Run on arbitrary text."""
# This runs when first sending the prompt
# to the LLM, adding it will send the final prompt
# to the frontend
async def on_agent_action(self, action: AgentAction, **kwargs: Any):
log = f"Thought: {action.log}"
# if there are line breaks, split them and send them
# as separate messages
if "\n" in log:
logs = log.split("\n")
for log in logs:
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
await self.websocket.send_json(resp.dict())
else:
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
await self.websocket.send_json(resp.dict())
async def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any:
"""Run on agent end."""
resp = ChatResponse(
message="",
type="stream",
intermediate_steps=finish.log,
)
await self.websocket.send_json(resp.dict())
class StreamingLLMCallbackHandler(BaseCallbackHandler):
"""Callback handler for streaming LLM responses."""

View file

@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketException, status
from fastapi.responses import StreamingResponse
from langflow.api.utils import build_input_keys_response
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
from langflow.chat.manager import ChatManager
@ -83,6 +84,7 @@ async def stream_build(flow_id: str):
async def event_stream(flow_id):
final_response = {"end_of_stream": True}
artifacts = {}
try:
if flow_id not in flow_data_store:
error_message = "Invalid session ID"
@ -113,13 +115,6 @@ async def stream_build(flow_id: str):
number_of_nodes = len(graph.nodes)
flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS
# To deal with the ZeroShotAgent case
# we need to build the root node first
# and then the rest of the graph
# This is a big problem because certain nodes require
# params that are not connected to it.
# We should consider connecting the tools to the ZeroShotPrompt
graph.build()
for i, vertex in enumerate(graph.generator_build(), 1):
try:
@ -131,8 +126,13 @@ async def stream_build(flow_id: str):
params = vertex._built_object_repr()
valid = True
logger.debug(
f"Building node {params[:50]}{'...' if len(params) > 50 else ''}"
f"Building node {str(params)[:50]}{'...' if len(str(params)) > 50 else ''}"
)
if vertex.artifacts:
# The artifacts will be prompt variables
# passed to build_input_keys_response
# to set the input_keys values
artifacts.update(vertex.artifacts)
except Exception as exc:
params = str(exc)
valid = False
@ -147,9 +147,26 @@ async def stream_build(flow_id: str):
yield str(StreamData(event="message", data=response))
chat_manager.set_cache(flow_id, graph.build())
langchain_object = graph.build()
# Now we need to check the input_keys to send them to the client
if hasattr(langchain_object, "input_keys"):
input_keys_response = build_input_keys_response(
langchain_object, artifacts
)
else:
input_keys_response = {
"input_keys": {},
"memory_keys": [],
"handle_keys": [],
}
yield str(StreamData(event="message", data=input_keys_response))
chat_manager.set_cache(flow_id, langchain_object)
# We need to reset the chat history
chat_manager.chat_history.empty_history(flow_id)
flow_data_store[flow_id]["status"] = BuildStatus.SUCCESS
except Exception as exc:
logger.exception(exc)
logger.error("Error while building the flow: %s", exc)
flow_data_store[flow_id]["status"] = BuildStatus.FAILURE
yield str(StreamData(event="error", data={"error": str(exc)}))

View file

@ -53,7 +53,7 @@ class ChatMessage(BaseModel):
"""Chat message schema."""
is_bot: bool = False
message: Union[str, None] = None
message: Union[str, None, dict] = None
type: str = "human"

View file

@ -3,10 +3,11 @@ from fastapi import APIRouter, HTTPException
from langflow.api.v1.base import (
Code,
CodeValidationResponse,
Prompt,
ValidatePromptRequest,
PromptValidationResponse,
validate_prompt,
)
from langflow.template.field.base import TemplateField
from langflow.utils.logger import logger
from langflow.utils.validate import validate_code
@ -27,9 +28,100 @@ def post_validate_code(code: Code):
@router.post("/prompt", status_code=200, response_model=PromptValidationResponse)
def post_validate_prompt(prompt: Prompt):
def post_validate_prompt(prompt_request: ValidatePromptRequest):
try:
return validate_prompt(prompt.template)
input_variables = validate_prompt(prompt_request.template)
old_custom_fields = get_old_custom_fields(prompt_request)
add_new_variables_to_template(input_variables, prompt_request)
remove_old_variables_from_template(
old_custom_fields, input_variables, prompt_request
)
update_input_variables_field(input_variables, prompt_request)
return PromptValidationResponse(
input_variables=input_variables,
frontend_node=prompt_request.frontend_node,
)
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e
def get_old_custom_fields(prompt_request):
try:
old_custom_fields = prompt_request.frontend_node.custom_fields[
prompt_request.name
].copy()
except KeyError:
old_custom_fields = []
prompt_request.frontend_node.custom_fields[prompt_request.name] = []
return old_custom_fields
def add_new_variables_to_template(input_variables, prompt_request):
for variable in input_variables:
try:
template_field = TemplateField(
name=variable,
display_name=variable,
field_type="str",
show=True,
advanced=False,
multiline=True,
input_types=["Document", "BaseOutputParser"],
value="", # Set the value to empty string
)
if variable in prompt_request.frontend_node.template:
# Set the new field with the old value
template_field.value = prompt_request.frontend_node.template[variable][
"value"
]
prompt_request.frontend_node.template[variable] = template_field.to_dict()
# Check if variable is not already in the list before appending
if (
variable
not in prompt_request.frontend_node.custom_fields[prompt_request.name]
):
prompt_request.frontend_node.custom_fields[prompt_request.name].append(
variable
)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
def remove_old_variables_from_template(
old_custom_fields, input_variables, prompt_request
):
for variable in old_custom_fields:
if variable not in input_variables:
try:
# Remove the variable from custom_fields associated with the given name
if (
variable
in prompt_request.frontend_node.custom_fields[prompt_request.name]
):
prompt_request.frontend_node.custom_fields[
prompt_request.name
].remove(variable)
# Remove the variable from the template
prompt_request.frontend_node.template.pop(variable, None)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
def update_input_variables_field(input_variables, prompt_request):
if "input_variables" in prompt_request.frontend_node.template:
prompt_request.frontend_node.template["input_variables"][
"value"
] = input_variables

View file

@ -104,16 +104,22 @@ class ChatManager:
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)
try:
await websocket.close(code=code, reason=reason)
self.disconnect(client_id)
except RuntimeError as exc:
# This is to catch the following error:
# Unexpected ASGI message 'websocket.close', after sending 'websocket.close'
if "after sending" in str(exc):
logger.error(exc)
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)
chat_inputs = payload.pop("inputs", "")
chat_inputs = ChatMessage(message=chat_inputs)
self.chat_history.add_message(client_id, chat_inputs)
# graph_data = payload
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
@ -126,7 +132,7 @@ class ChatManager:
result, intermediate_steps = await process_graph(
langchain_object=langchain_object,
chat_message=chat_message,
chat_inputs=chat_inputs,
websocket=self.active_connections[client_id],
)
except Exception as e:

View file

@ -7,7 +7,7 @@ from langflow.utils.logger import logger
async def process_graph(
langchain_object,
chat_message: ChatMessage,
chat_inputs: ChatMessage,
websocket: WebSocket,
):
langchain_object = try_setting_streaming_options(langchain_object, websocket)
@ -21,9 +21,13 @@ async def process_graph(
# Generate result and thought
try:
if not chat_inputs.message:
logger.debug("No message provided")
raise ValueError("No message provided")
logger.debug("Generating result and thought")
result, intermediate_steps = await get_result_and_steps(
langchain_object, chat_message.message or "", websocket=websocket
langchain_object, chat_inputs.message, websocket=websocket
)
logger.debug("Generated result and intermediate_steps")
return result, intermediate_steps

View file

@ -151,12 +151,19 @@ memories:
documentation: "https://python.langchain.com/docs/modules/memory/how_to/buffer_window"
VectorStoreRetrieverMemory:
documentation: "https://python.langchain.com/docs/modules/memory/how_to/vectorstore_retriever_memory"
MongoDBChatMessageHistory:
documentation: "https://python.langchain.com/docs/modules/memory/integrations/mongodb_chat_message_history"
prompts:
ChatMessagePromptTemplate:
documentation: "https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/msg_prompt_templates"
HumanMessagePromptTemplate:
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/how_to/prompts"
SystemMessagePromptTemplate:
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/how_to/prompts"
ChatPromptTemplate:
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/how_to/prompts"
PromptTemplate:
documentation: "https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/"
ZeroShotPrompt:
documentation: "https://python.langchain.com/docs/modules/agents/how_to/custom_mrkl_agent"
textsplitters:
CharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"
@ -269,7 +276,17 @@ vectorstores:
SupabaseVectorStore:
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/supabase"
MongoDBAtlasVectorSearch:
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/mongodb_atlas_vector_search"
documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/mongodb_atlas"
# Requires docarray >=0.32.0 but langchain-serve requires jina 3.15.2 which doesn't support docarray >=0.32.0
# DocArrayInMemorySearch:
# documentation: "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/docarray_in_memory"
wrappers:
RequestsWrapper:
documentation: ""
SQLDatabase:
documentation: ""
output_parsers:
StructuredOutputParser:
documentation: "https://python.langchain.com/docs/modules/model_io/output_parsers/structured"
ResponseSchema:
documentation: "https://python.langchain.com/docs/modules/model_io/output_parsers/structured"

View file

@ -2,9 +2,9 @@ from langflow.template import frontend_node
# These should always be instantiated
CUSTOM_NODES = {
"prompts": {
"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode(),
},
# "prompts": {
# "ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode(),
# },
"tools": {
"PythonFunctionTool": frontend_node.tools.PythonFunctionToolNode(),
"PythonFunction": frontend_node.tools.PythonFunctionNode(),
@ -23,6 +23,7 @@ CUSTOM_NODES = {
},
"memories": {
"PostgresChatMessageHistory": frontend_node.memories.PostgresChatMessageHistoryFrontendNode(),
"MongoDBChatMessageHistory": frontend_node.memories.MongoDBChatMessageHistoryFrontendNode(),
},
"chains": {
"SeriesCharacterChain": frontend_node.chains.SeriesCharacterChainNode(),

View file

@ -6,9 +6,15 @@ if TYPE_CHECKING:
class Edge:
def __init__(self, source: "Vertex", target: "Vertex"):
def __init__(self, source: "Vertex", target: "Vertex", edge: dict):
self.source: "Vertex" = source
self.target: "Vertex" = target
self.source_handle = edge.get("sourceHandle", "")
self.target_handle = edge.get("targetHandle", "")
# 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD'
# target_param is documents
self.target_param = self.target_handle.split("|")[1]
self.validate_edge()
def validate_edge(self) -> None:
@ -42,6 +48,16 @@ class Edge:
def __repr__(self) -> str:
return (
f"Edge(source={self.source.id}, target={self.target.id}, valid={self.valid}"
f"Edge(source={self.source.id}, target={self.target.id}, target_param={self.target_param}"
f", matched_type={self.matched_type})"
)
def __hash__(self) -> int:
return hash(self.__repr__())
def __eq__(self, __value: object) -> bool:
return (
self.__repr__() == __value.__repr__()
if isinstance(__value, Edge)
else False
)

View file

@ -179,7 +179,7 @@ class Graph:
raise ValueError(f"Source node {edge['source']} not found")
if target is None:
raise ValueError(f"Target node {edge['target']} not found")
edges.append(Edge(source, target))
edges.append(Edge(source, target, edge))
return edges
def _get_vertex_class(self, node_type: str, node_lc_type: str) -> Type[Vertex]:
@ -214,3 +214,10 @@ class Graph:
if node_type in node_types:
children.append(node)
return children
def __repr__(self):
node_ids = [node.id for node in self.nodes]
edges_repr = "\n".join(
[f"{edge.source.id} --> {edge.target.id}" for edge in self.edges]
)
return f"Graph:\nNodes: {node_ids}\nConnections:\n{edges_repr}"

View file

@ -12,6 +12,7 @@ from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.tools.base import tool_creator
from langflow.interface.vector_store.base import vectorstore_creator
from langflow.interface.wrappers.base import wrapper_creator
from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.retrievers.base import retriever_creator
from typing import Dict, Type
@ -30,5 +31,6 @@ VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = {
**{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()},
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
}

View file

@ -1,14 +1,12 @@
from langflow.utils.constants import DIRECT_TYPES
from langflow.interface.initialize import loading
from langflow.interface.listing import ALL_TYPES_DICT
from langflow.utils.constants import DIRECT_TYPES
from langflow.utils.logger import logger
from langflow.utils.util import sync_to_async
import contextlib
import inspect
import types
import warnings
from typing import Any, Dict, List, Optional
from typing import TYPE_CHECKING
@ -25,6 +23,7 @@ class Vertex:
self._parse_data()
self._built_object = None
self._built = False
self.artifacts: Dict[str, Any] = {}
def _parse_data(self) -> None:
self.data = self._data["data"]
@ -45,6 +44,14 @@ class Vertex:
for key, value in template_dicts.items()
if not value["required"]
]
# Add the template_dicts[key]["input_types"] to the optional_inputs
self.optional_inputs.extend(
[
input_type
for value in template_dicts.values()
for input_type in value.get("input_types", [])
]
)
template_dict = self.data["node"]["template"]
self.vertex_type = (
@ -60,6 +67,7 @@ class Vertex:
break
def _build_params(self):
# sourcery skip: merge-list-append, remove-redundant-if
# Some params are required, some are optional
# but most importantly, some params are python base classes
# like str and others are LangChain objects like LLMChain, BasePromptTemplate
@ -80,8 +88,19 @@ class Vertex:
if isinstance(value, dict)
}
params = {}
for edge in self.edges:
param_key = edge.target_param
if param_key in template_dict:
if template_dict[param_key]["list"]:
if param_key not in params:
params[param_key] = []
params[param_key].append(edge.source)
elif edge.target.id == self.id:
params[param_key] = edge.source
for key, value in template_dict.items():
if key == "_type":
if key == "_type" or not value.get("show"):
continue
# If the type is not transformable to a python base class
# then we need to get the edge that connects to this node
@ -92,124 +111,131 @@ class Vertex:
file_path = value.get("file_path")
params[key] = file_path
elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
params[key] = value.get("value")
elif value.get("type") not in DIRECT_TYPES:
# Get the edge that connects to this node
edges = [
edge
for edge in self.edges
if edge.target == self and edge.matched_type in value["type"]
]
# Get the output of the node that the edge connects to
# if the value['list'] is True, then there will be more
# than one time setting to params[key]
# so we need to append to a list if it exists
# or create a new list if it doesn't
if value["required"] and not edges:
# If a required parameter is not found, raise an error
raise ValueError(
f"Required input {key} for module {self.vertex_type} not found"
)
elif value["list"]:
# If this is a list parameter, append all sources to a list
params[key] = [edge.source for edge in edges]
elif edges:
# If a single parameter is found, use its source
params[key] = edges[0].source
elif value["required"] or value.get("value"):
# If value does not have value this still passes
# but then gives a keyError
# so we need to check if value has value
new_value = value.get("value")
if new_value is None:
warnings.warn(f"Value for {key} in {self.vertex_type} is None. ")
if value.get("type") == "int":
with contextlib.suppress(TypeError, ValueError):
new_value = int(new_value) # type: ignore
params[key] = new_value
if not value.get("required") and params.get(key) is None:
if value.get("default"):
params[key] = value.get("default")
else:
params.pop(key, None)
# Add _type to params
self.params = params
def _build(self):
# The params dict is used to build the module
# it contains values and keys that point to nodes which
# have their own params dict
# When build is called, we iterate through the params dict
# and if the value is a node, we call build on that node
# and use the output of that build as the value for the param
# if the value is not a node, then we use the value as the param
# and continue
# Another aspect is that the node_type is the class that we need to import
# and instantiate with these built params
"""
Initiate the build process.
"""
logger.debug(f"Building {self.vertex_type}")
# Build each node in the params dict
self._build_each_node_in_params_dict()
self._get_and_instantiate_class()
self._validate_built_object()
self._built = True
def _build_each_node_in_params_dict(self):
"""
Iterates over each node in the params dictionary and builds it.
"""
for key, value in self.params.copy().items():
# Check if Node or list of Nodes and not self
# to avoid recursion
if isinstance(value, Vertex):
if self._is_node(value):
if value == self:
del self.params[key]
continue
result = value.build()
# If the key is "func", then we need to use the run method
if key == "func":
if not isinstance(result, types.FunctionType):
# func can be
# PythonFunction(code='\ndef upper_case(text: str) -> str:\n return text.upper()\n')
# so we need to check if there is an attribute called run
if hasattr(result, "run"):
result = result.run # type: ignore
elif hasattr(result, "get_function"):
result = result.get_function() # type: ignore
elif inspect.iscoroutinefunction(result):
self.params["coroutine"] = result
else:
# turn result which is a function into a coroutine
# so that it can be awaited
self.params["coroutine"] = sync_to_async(result)
if isinstance(result, list):
# If the result is a list, then we need to extend the list
# with the result but first check if the key exists
# if it doesn't, then we need to create a new list
if isinstance(self.params[key], list):
self.params[key].extend(result)
self._build_node_and_update_params(key, value)
elif isinstance(value, list) and self._is_list_of_nodes(value):
self._build_list_of_nodes_and_update_params(key, value)
self.params[key] = result
elif isinstance(value, list) and all(
isinstance(node, Vertex) for node in value
):
self.params[key] = []
for node in value:
built = node.build()
if isinstance(built, list):
self.params[key].extend(built)
else:
self.params[key].append(built)
def _is_node(self, value):
"""
Checks if the provided value is an instance of Vertex.
"""
return isinstance(value, Vertex)
# Get the class from LANGCHAIN_TYPES_DICT
# and instantiate it with the params
# and return the instance
def _is_list_of_nodes(self, value):
"""
Checks if the provided value is a list of Vertex instances.
"""
return all(self._is_node(node) for node in value)
def _build_node_and_update_params(self, key, node):
"""
Builds a given node and updates the params dictionary accordingly.
"""
result = node.build()
self._handle_func(key, result)
if isinstance(result, list):
self._extend_params_list_with_result(key, result)
self.params[key] = result
def _build_list_of_nodes_and_update_params(self, key, nodes):
"""
Iterates over a list of nodes, builds each and updates the params dictionary.
"""
self.params[key] = []
for node in nodes:
built = node.build()
if isinstance(built, list):
self.params[key].extend(built)
else:
self.params[key].append(built)
def _handle_func(self, key, result):
"""
Handles 'func' key by checking if the result is a function and setting it as coroutine.
"""
if key == "func":
if not isinstance(result, types.FunctionType):
if hasattr(result, "run"):
result = result.run # type: ignore
elif hasattr(result, "get_function"):
result = result.get_function() # type: ignore
elif inspect.iscoroutinefunction(result):
self.params["coroutine"] = result
else:
self.params["coroutine"] = sync_to_async(result)
def _extend_params_list_with_result(self, key, result):
"""
Extends a list in the params dictionary with the given result if it exists.
"""
if isinstance(self.params[key], list):
self.params[key].extend(result)
def _get_and_instantiate_class(self):
"""
Gets the class from a dictionary and instantiates it with the params.
"""
if self.base_type is None:
raise ValueError(f"Base type for node {self.vertex_type} not found")
try:
self._built_object = loading.instantiate_class(
result = loading.instantiate_class(
node_type=self.vertex_type,
base_type=self.base_type,
params=self.params,
)
self._update_built_object_and_artifacts(result)
except Exception as exc:
raise ValueError(
f"Error building node {self.vertex_type}: {str(exc)}"
) from exc
def _update_built_object_and_artifacts(self, result):
"""
Updates the built object and its artifacts.
"""
if isinstance(result, tuple):
self._built_object, self.artifacts = result
else:
self._built_object = result
def _validate_built_object(self):
"""
Checks if the built object is None and raises a ValueError if so.
"""
if self._built_object is None:
raise ValueError(f"Node type {self.vertex_type} not found")
self._built = True
def build(self, force: bool = False) -> Any:
if not self._built or force:
self._build()
@ -217,7 +243,8 @@ class Vertex:
return self._built_object
def add_edge(self, edge: "Edge") -> None:
self.edges.append(edge)
if edge not in self.edges:
self.edges.append(edge)
def __repr__(self) -> str:
return f"Vertex(id={self.id}, data={self.data})"
@ -229,4 +256,5 @@ class Vertex:
return id(self)
def _built_object_repr(self):
return repr(self._built_object)
# Add a message with an emoji, stars for sucess,
return "Built sucessfully ✨" if self._built_object else "Failed to build 😵‍💫"

View file

@ -1,3 +1,4 @@
import ast
from typing import Any, Dict, List, Optional, Union
from langflow.graph.vertex.base import Vertex
@ -79,7 +80,7 @@ class WrapperVertex(Vertex):
def build(self, force: bool = False) -> Any:
if not self._built or force:
if "headers" in self.params:
self.params["headers"] = eval(self.params["headers"])
self.params["headers"] = ast.literal_eval(self.params["headers"])
self._build()
return self._built_object
@ -91,8 +92,13 @@ class DocumentLoaderVertex(Vertex):
def _built_object_repr(self):
# This built_object is a list of documents. Maybe we should
# show how many documents are in the list?
if self._built_object:
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
self._built_object
)
return f"""{self.vertex_type}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {avg_length}
Documents: {self._built_object[:3]}..."""
return f"{self.vertex_type}()"
@ -124,8 +130,13 @@ class TextSplitterVertex(Vertex):
def _built_object_repr(self):
# This built_object is a list of documents. Maybe we should
# show how many documents are in the list?
if self._built_object:
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
self._built_object
)
return f"""{self.vertex_type}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {avg_length}
\nDocuments: {self._built_object[:3]}..."""
return f"{self.vertex_type}()"
@ -185,11 +196,46 @@ class PromptVertex(Vertex):
]
else:
prompt_params = ["template"]
for param in prompt_params:
prompt_text = self.params[param]
variables = extract_input_variables_from_prompt(prompt_text)
self.params["input_variables"].extend(variables)
self.params["input_variables"] = list(set(self.params["input_variables"]))
if "prompt" not in self.params and "messages" not in self.params:
for param in prompt_params:
prompt_text = self.params[param]
variables = extract_input_variables_from_prompt(prompt_text)
self.params["input_variables"].extend(variables)
self.params["input_variables"] = list(
set(self.params["input_variables"])
)
else:
self.params.pop("input_variables", None)
self._build()
return self._built_object
def _built_object_repr(self):
if (
not self.artifacts
or self._built_object is None
or not hasattr(self._built_object, "format")
):
return super()._built_object_repr()
# We'll build the prompt with the artifacts
# to show the user what the prompt looks like
# with the variables filled in
artifacts = self.artifacts.copy()
# Remove the handle_keys from the artifacts
# so the prompt format doesn't break
artifacts.pop("handle_keys", None)
try:
template = self._built_object.format(**artifacts)
return (
template
if isinstance(template, str)
else f"{self.vertex_type}({template})"
)
except KeyError:
return str(self._built_object)
class OutputParserVertex(Vertex):
def __init__(self, data: Dict):
super().__init__(data, base_type="output_parsers")

View file

@ -6,13 +6,20 @@ from langflow.custom.customs import get_custom_nodes
from langflow.interface.agents.custom import CUSTOM_AGENTS
from langflow.interface.base import LangChainTypeCreator
from langflow.settings import settings
from langflow.template.frontend_node.agents import AgentFrontendNode
from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class
from langflow.utils.util import build_template_from_class, build_template_from_method
class AgentCreator(LangChainTypeCreator):
type_name: str = "agents"
from_method_nodes = {"ZeroShotAgent": "from_llm_and_tools"}
@property
def frontend_node_class(self) -> type[AgentFrontendNode]:
return AgentFrontendNode
@property
def type_to_loader_dict(self) -> Dict:
if self.type_dict is None:
@ -27,6 +34,13 @@ class AgentCreator(LangChainTypeCreator):
try:
if name in get_custom_nodes(self.type_name).keys():
return get_custom_nodes(self.type_name)[name]
elif name in self.from_method_nodes:
return build_template_from_method(
name,
type_to_cls_dict=self.type_to_loader_dict,
add_function=True,
method_name=self.from_method_nodes[name],
)
return build_template_from_class(
name, self.type_to_loader_dict, add_function=True
)

View file

@ -157,7 +157,7 @@ class VectorStoreAgent(CustomAgentExecutor):
llm_chain=llm_chain, allowed_tools=tool_names, **kwargs # type: ignore
)
return AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, verbose=True
agent=agent, tools=tools, verbose=True, handle_parsing_errors=True
)
def run(self, *args, **kwargs):
@ -232,6 +232,7 @@ class SQLAgent(CustomAgentExecutor):
verbose=True,
max_iterations=15,
early_stopping_method="force",
handle_parsing_errors=True,
)
def run(self, *args, **kwargs):
@ -276,7 +277,7 @@ class VectorStoreRouterAgent(CustomAgentExecutor):
llm_chain=llm_chain, allowed_tools=tool_names, **kwargs # type: ignore
)
return AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, verbose=True
agent=agent, tools=tools, verbose=True, handle_parsing_errors=True
)
def run(self, *args, **kwargs):
@ -308,6 +309,7 @@ class InitializeAgent(CustomAgentExecutor):
agent=agent, # type: ignore
memory=memory,
return_intermediate_steps=True,
handle_parsing_errors=True,
)
def __init__(self, *args, **kwargs):

View file

@ -23,6 +23,7 @@ class ChainCreator(LangChainTypeCreator):
from_method_nodes = {
"ConversationalRetrievalChain": "from_llm",
"LLMCheckerChain": "from_llm",
"SQLDatabaseChain": "from_llm",
}
@property

View file

@ -10,6 +10,7 @@ from langchain.chains.base import Chain
from langchain.chat_models.base import BaseChatModel
from langchain.tools import BaseTool
from langflow.utils import validate
from langflow.interface.wrappers.base import wrapper_creator
def import_module(module_path: str) -> Any:
@ -44,6 +45,7 @@ def import_by_type(_type: str, name: str) -> Any:
"documentloaders": import_documentloader,
"textsplitters": import_textsplitter,
"utilities": import_utility,
"output_parsers": import_output_parser,
"retrievers": import_retriever,
}
if _type == "llms":
@ -55,6 +57,11 @@ def import_by_type(_type: str, name: str) -> Any:
return loaded_func(name)
def import_output_parser(output_parser: str) -> Any:
"""Import output parser from output parser name"""
return import_module(f"from langchain.output_parsers import {output_parser}")
def import_chat_llm(llm: str) -> BaseChatModel:
"""Import chat llm from llm name"""
return import_class(f"langchain.chat_models.{llm}")
@ -90,7 +97,11 @@ def import_prompt(prompt: str) -> Type[PromptTemplate]:
def import_wrapper(wrapper: str) -> Any:
"""Import wrapper from wrapper name"""
return import_module(f"from langchain.requests import {wrapper}")
if (
isinstance(wrapper_creator.type_dict, dict)
and wrapper in wrapper_creator.type_dict
):
return wrapper_creator.type_dict.get(wrapper)
def import_toolkit(toolkit: str) -> Any:

View file

@ -1,5 +1,6 @@
import contextlib
import json
from typing import Any, Callable, Dict, Sequence, Type
from typing import Any, Callable, Dict, List, Sequence, Type
from langchain.agents import ZeroShotAgent
from langchain.agents import agent as agent_module
@ -10,19 +11,22 @@ from langflow.interface.initialize.llm import initialize_vertexai
from langflow.interface.initialize.vector_store import vecstore_initializer
from langchain.schema import Document, BaseOutputParser
from pydantic import ValidationError
from langflow.interface.custom_lists import CUSTOM_NODES
from langflow.interface.importing.utils import get_function, import_by_type
from langflow.interface.agents.base import agent_creator
from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.retrievers.base import retriever_creator
from langflow.interface.wrappers.base import wrapper_creator
from langflow.interface.utils import load_file_into_dict
from langflow.utils import validate
from langchain.chains.base import Chain
from langchain.vectorstores.base import VectorStore
from langchain.document_loaders.base import BaseLoader
from langchain.prompts.base import BasePromptTemplate
def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
@ -60,11 +64,15 @@ def convert_kwargs(params):
def instantiate_based_on_type(class_object, base_type, node_type, params):
if base_type == "agents":
return instantiate_agent(class_object, params)
return instantiate_agent(node_type, class_object, params)
elif base_type == "prompts":
return instantiate_prompt(node_type, class_object, params)
elif base_type == "tools":
return instantiate_tool(node_type, class_object, params)
tool = instantiate_tool(node_type, class_object, params)
if hasattr(tool, "name") and isinstance(tool, BaseTool):
# tool name shouldn't contain spaces
tool.name = tool.name.replace(" ", "_")
return tool
elif base_type == "toolkits":
return instantiate_toolkit(node_type, class_object, params)
elif base_type == "embeddings":
@ -79,26 +87,63 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
return instantiate_utility(node_type, class_object, params)
elif base_type == "chains":
return instantiate_chains(node_type, class_object, params)
elif base_type == "output_parsers":
return instantiate_output_parser(node_type, class_object, params)
elif base_type == "llms":
return instantiate_llm(node_type, class_object, params)
elif base_type == "retrievers":
return instantiate_retriever(node_type, class_object, params)
elif base_type == "memory":
return instantiate_memory(node_type, class_object, params)
elif base_type == "wrappers":
return instantiate_wrapper(node_type, class_object, params)
else:
return class_object(**params)
def instantiate_wrapper(node_type, class_object, params):
if node_type in wrapper_creator.from_method_nodes:
method = wrapper_creator.from_method_nodes[node_type]
if class_method := getattr(class_object, method, None):
return class_method(**params)
raise ValueError(f"Method {method} not found in {class_object}")
return class_object(**params)
def instantiate_output_parser(node_type, class_object, params):
if node_type in output_parser_creator.from_method_nodes:
method = output_parser_creator.from_method_nodes[node_type]
if class_method := getattr(class_object, method, None):
return class_method(**params)
raise ValueError(f"Method {method} not found in {class_object}")
return class_object(**params)
def instantiate_llm(node_type, class_object, params: Dict):
# This is a workaround so JinaChat works until streaming is implemented
# if "openai_api_base" in params and "jina" in params["openai_api_base"]:
# False if condition is True
if node_type == "VertexAI":
return initialize_vertexai(class_object=class_object, params=params)
# max_tokens sometimes is a string and should be an int
if "max_tokens" in params:
if isinstance(params["max_tokens"], str) and params["max_tokens"].isdigit():
params["max_tokens"] = int(params["max_tokens"])
elif not isinstance(params.get("max_tokens"), int):
params.pop("max_tokens", None)
return class_object(**params)
def instantiate_memory(node_type, class_object, params):
# process input_key and output_key to remove them if
# they are empty strings
if node_type == "ConversationEntityMemory":
params.pop("memory_key", None)
for key in ["input_key", "output_key"]:
if key in params and (params[key] == "" or not params[key]):
params.pop(key)
try:
if "retriever" in params and hasattr(params["retriever"], "as_retriever"):
params["retriever"] = params["retriever"].as_retriever()
@ -141,26 +186,94 @@ def instantiate_chains(node_type, class_object: Type[Chain], params: Dict):
return class_object(**params)
def instantiate_agent(class_object: Type[agent_module.Agent], params: Dict):
def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params: Dict):
if node_type in agent_creator.from_method_nodes:
method = agent_creator.from_method_nodes[node_type]
if class_method := getattr(class_object, method, None):
agent = class_method(**params)
tools = params.get("tools", [])
return AgentExecutor.from_agent_and_tools(
agent=agent, tools=tools, handle_parsing_errors=True
)
return load_agent_executor(class_object, params)
def instantiate_prompt(node_type, class_object: Type[BasePromptTemplate], params: Dict):
def instantiate_prompt(node_type, class_object, params: Dict):
if node_type == "ZeroShotPrompt":
if "tools" not in params:
params["tools"] = []
return ZeroShotAgent.create_prompt(**params)
return class_object(**params)
elif "MessagePromptTemplate" in node_type:
# Then we only need the template
from_template_params = {
"template": params.pop("prompt", params.pop("template", ""))
}
if not from_template_params.get("template"):
raise ValueError("Prompt template is required")
prompt = class_object.from_template(**from_template_params)
elif node_type == "ChatPromptTemplate":
prompt = class_object.from_messages(**params)
else:
prompt = class_object(**params)
format_kwargs: Dict[str, Any] = {}
for input_variable in prompt.input_variables:
if input_variable in params:
variable = params[input_variable]
if isinstance(variable, str):
format_kwargs[input_variable] = variable
elif isinstance(variable, BaseOutputParser) and hasattr(
variable, "get_format_instructions"
):
format_kwargs[input_variable] = variable.get_format_instructions()
elif isinstance(variable, List) and all(
isinstance(item, Document) for item in variable
):
# Format document to contain page_content and metadata
# as one string separated by a newline
if len(variable) > 1:
content = "\n".join(
[item.page_content for item in variable if item.page_content]
)
else:
content = variable[0].page_content
# content could be a json list of strings
with contextlib.suppress(json.JSONDecodeError):
content = json.loads(content)
if isinstance(content, list):
content = ",".join([str(item) for item in content])
format_kwargs[input_variable] = content
# handle_keys will be a list but it does not exist yet
# so we need to create it
if (
isinstance(variable, List)
and all(isinstance(item, Document) for item in variable)
) or (
isinstance(variable, BaseOutputParser)
and hasattr(variable, "get_format_instructions")
):
if "handle_keys" not in format_kwargs:
format_kwargs["handle_keys"] = []
# Add the handle_keys to the list
format_kwargs["handle_keys"].append(input_variable)
return prompt, format_kwargs
def instantiate_tool(node_type, class_object: Type[BaseTool], params: Dict):
if node_type == "JsonSpec":
params["dict_"] = load_file_into_dict(params.pop("path"))
if file_dict := load_file_into_dict(params.pop("path")):
params["dict_"] = file_dict
else:
raise ValueError("Invalid file")
return class_object(**params)
elif node_type == "PythonFunctionTool":
params["func"] = get_function(params.get("code"))
return class_object(**params)
# For backward compatibility
elif node_type == "PythonFunction":
function_string = params["code"]
if isinstance(function_string, str):
@ -218,7 +331,7 @@ def instantiate_documentloader(class_object: Type[BaseLoader], params: Dict):
# like lambda x: x.endswith(".txt") but as we don't know
# anything besides the string, we will simply check if the string is
# in x and if it is, we will return True
file_filter = params.pop("file_filter", None)
file_filter = params.pop("file_filter")
extensions = file_filter.split(",")
params["file_filter"] = lambda x: any(
extension.strip() in x for extension in extensions
@ -260,6 +373,12 @@ def instantiate_textsplitter(
"separator_type" in params and params["separator_type"] == "Text"
) or "separator_type" not in params:
params.pop("separator_type", None)
# separators might come in as an escaped string like \\n
# so we need to convert it to a string
if "separators" in params:
params["separators"] = (
params["separators"].encode().decode("unicode-escape")
)
text_splitter = class_object(**params)
else:
from langchain.text_splitter import Language
@ -312,6 +431,7 @@ def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs)
return AgentExecutor.from_agent_and_tools(
agent=agent,
tools=allowed_tools,
handle_parsing_errors=True,
# memory=memory,
**kwargs,
)

View file

@ -11,6 +11,7 @@ from langflow.interface.tools.base import tool_creator
from langflow.interface.utilities.base import utility_creator
from langflow.interface.vector_store.base import vectorstore_creator
from langflow.interface.wrappers.base import wrapper_creator
from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.retrievers.base import retriever_creator
@ -29,6 +30,7 @@ def get_type_dict():
"embeddings": embedding_creator.to_list(),
"textSplitters": textsplitter_creator.to_list(),
"utilities": utility_creator.to_list(),
"outputParsers": output_parser_creator.to_list(),
"retrievers": retriever_creator.to_list(),
}

View file

@ -0,0 +1,64 @@
from typing import Dict, List, Optional, Type
from langchain import output_parsers
from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class
from langflow.settings import settings
from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode
from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class, build_template_from_method
class OutputParserCreator(LangChainTypeCreator):
type_name: str = "output_parsers"
from_method_nodes = {
"StructuredOutputParser": "from_response_schemas",
}
@property
def frontend_node_class(self) -> Type[OutputParserFrontendNode]:
return OutputParserFrontendNode
@property
def type_to_loader_dict(self) -> Dict:
if self.type_dict is None:
self.type_dict = {
output_parser_name: import_class(
f"langchain.output_parsers.{output_parser_name}"
)
# if output_parser_name is not lower case it is a class
for output_parser_name in output_parsers.__all__
}
self.type_dict = {
name: output_parser
for name, output_parser in self.type_dict.items()
if name in settings.output_parsers or settings.dev
}
return self.type_dict
def get_signature(self, name: str) -> Optional[Dict]:
try:
if name in self.from_method_nodes:
return build_template_from_method(
name,
type_to_cls_dict=self.type_to_loader_dict,
method_name=self.from_method_nodes[name],
)
else:
return build_template_from_class(
name,
type_to_cls_dict=self.type_to_loader_dict,
)
except ValueError as exc:
# raise ValueError("OutputParser not found") from exc
logger.error(f"OutputParser {name} not found: {exc}")
except AttributeError as exc:
logger.error(f"OutputParser {name} not loaded: {exc}")
return None
def to_list(self) -> List[str]:
return list(self.type_to_loader_dict.keys())
output_parser_creator = OutputParserCreator()

View file

@ -14,6 +14,23 @@ def build_langchain_object_with_caching(data_graph):
return graph.build()
@memoize_dict(maxsize=10)
def build_sorted_vertices_with_caching(data_graph):
"""
Build langchain object from data_graph.
"""
logger.debug("Building langchain object")
graph = Graph.from_payload(data_graph)
sorted_vertices = graph.topological_sort()
artifacts = {}
for vertex in sorted_vertices:
vertex.build()
if vertex.artifacts:
artifacts.update(vertex.artifacts)
return graph.build(), artifacts
def build_langchain_object(data_graph):
"""
Build langchain object from data_graph.

View file

@ -90,7 +90,7 @@ class ToolCreator(LangChainTypeCreator):
def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of a tool."""
base_classes = ["Tool"]
base_classes = ["Tool", "BaseTool"]
fields = []
params = []
tool_params = {}

View file

@ -11,6 +11,7 @@ from langflow.interface.tools.base import tool_creator
from langflow.interface.utilities.base import utility_creator
from langflow.interface.vector_store.base import vectorstore_creator
from langflow.interface.wrappers.base import wrapper_creator
from langflow.interface.output_parsers.base import output_parser_creator
from langflow.interface.retrievers.base import retriever_creator
@ -45,6 +46,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
documentloader_creator,
textsplitter_creator,
utility_creator,
output_parser_creator,
retriever_creator,
]

View file

@ -16,17 +16,15 @@ def load_file_into_dict(file_path: str) -> dict:
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
file_extension = os.path.splitext(file_path)[1].lower()
if file_extension == ".json":
with open(file_path, "r") as json_file:
data = json.load(json_file)
elif file_extension in [".yaml", ".yml"]:
with open(file_path, "r") as yaml_file:
data = yaml.safe_load(yaml_file)
else:
raise ValueError("Unsupported file type. Please provide a JSON or YAML file.")
# Files names are UUID, so we can't find the extension
with open(file_path, "r") as file:
try:
data = json.load(file)
except json.JSONDecodeError:
file.seek(0)
data = yaml.safe_load(file)
except ValueError as exc:
raise ValueError("Invalid file type. Expected .json or .yaml.") from exc
return data

View file

@ -1,25 +1,36 @@
from typing import Dict, List, Optional
from langchain import requests
from langchain import requests, sql_database
from langflow.interface.base import LangChainTypeCreator
from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class
from langflow.utils.util import build_template_from_class, build_template_from_method
class WrapperCreator(LangChainTypeCreator):
type_name: str = "wrappers"
from_method_nodes = {"SQLDatabase": "from_uri"}
@property
def type_to_loader_dict(self) -> Dict:
if self.type_dict is None:
self.type_dict = {
wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper]
wrapper.__name__: wrapper
for wrapper in [requests.TextRequestsWrapper, sql_database.SQLDatabase]
}
return self.type_dict
def get_signature(self, name: str) -> Optional[Dict]:
try:
if name in self.from_method_nodes:
return build_template_from_method(
name,
type_to_cls_dict=self.type_to_loader_dict,
add_function=True,
method_name=self.from_method_nodes[name],
)
return build_template_from_class(name, self.type_to_loader_dict)
except ValueError as exc:
raise ValueError("Wrapper not found") from exc

View file

@ -3,9 +3,9 @@
import os
# Use the JCLOUD_WORKSPACE for db URL if it's provided by JCloud.
if 'JCLOUD_WORKSPACE' in os.environ:
if "JCLOUD_WORKSPACE" in os.environ:
os.environ[
'LANGFLOW_DATABASE_URL'
"LANGFLOW_DATABASE_URL"
] = f"sqlite:///{os.environ['JCLOUD_WORKSPACE']}/langflow.db"
from langflow.main import setup_app

View file

@ -1,3 +1,4 @@
from typing import Union
from langflow.api.v1.callback import (
AsyncStreamingLLMCallbackHandler,
StreamingLLMCallbackHandler,
@ -6,39 +7,31 @@ from langflow.processing.process import fix_memory_inputs, format_actions
from langflow.utils.logger import logger
async def get_result_and_steps(langchain_object, message: str, **kwargs):
async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwargs):
"""Get result and thought from extracted json"""
try:
if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True
chat_input = None
memory_key = ""
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
memory_key = langchain_object.memory.memory_key
if hasattr(langchain_object, "input_keys"):
for key in langchain_object.input_keys:
if key not in [memory_key, "chat_history"]:
chat_input = {key: message}
else:
chat_input = message # type: ignore
if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068
# Deactivating until we have a frontend solution
# to display intermediate steps
langchain_object.return_intermediate_steps = True
try:
fix_memory_inputs(langchain_object)
except Exception as exc:
logger.error(exc)
fix_memory_inputs(langchain_object)
try:
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
output = await langchain_object.acall(chat_input, callbacks=async_callbacks)
output = await langchain_object.acall(inputs, callbacks=async_callbacks)
except Exception as exc:
# make the error message more informative
logger.debug(f"Error: {str(exc)}")
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
output = langchain_object(chat_input, callbacks=sync_callbacks)
output = langchain_object(inputs, callbacks=sync_callbacks)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []
@ -49,7 +42,11 @@ async def get_result_and_steps(langchain_object, message: str, **kwargs):
if isinstance(output, dict)
else output
)
thought = format_actions(intermediate_steps) if intermediate_steps else ""
try:
thought = format_actions(intermediate_steps) if intermediate_steps else ""
except Exception as exc:
logger.exception(exc)
thought = ""
except Exception as exc:
logger.exception(exc)
raise ValueError(f"Error: {str(exc)}") from exc

View file

@ -2,7 +2,7 @@ from pathlib import Path
from langchain.schema import AgentAction
import json
from langflow.interface.run import (
build_langchain_object_with_caching,
build_sorted_vertices_with_caching,
get_memory_key,
update_memory_keys,
)
@ -22,7 +22,10 @@ def fix_memory_inputs(langchain_object):
if not hasattr(langchain_object, "memory") or langchain_object.memory is None:
return
try:
if langchain_object.memory.memory_key in langchain_object.input_variables:
if (
hasattr(langchain_object.memory, "memory_key")
and langchain_object.memory.memory_key in langchain_object.input_variables
):
return
except AttributeError:
input_variables = (
@ -88,8 +91,20 @@ def process_graph_cached(data_graph: Dict[str, Any], inputs: Optional[dict] = No
with PromptTemplate,then run the graph and return the result and thought.
"""
# Load langchain object
langchain_object = build_langchain_object_with_caching(data_graph)
langchain_object, artifacts = build_sorted_vertices_with_caching(data_graph)
logger.debug("Loaded LangChain object")
if inputs is None:
inputs = {}
# Add artifacts to inputs
# artifacts can be documents loaded when building
# the flow
for (
key,
value,
) in artifacts.items():
if key not in inputs or not inputs[key]:
inputs[key] = value
if langchain_object is None:
# Raise user facing error
@ -105,8 +120,7 @@ def process_graph_cached(data_graph: Dict[str, Any], inputs: Optional[dict] = No
result = get_result_and_thought(langchain_object, inputs)
logger.debug("Generated result and thought")
elif isinstance(langchain_object, VectorStore):
class_name = langchain_object.__class__.__name__
result = {"message": f"Processed {class_name} successfully"}
result = langchain_object.search(**inputs)
else:
raise ValueError(
f"Unknown langchain_object type: {type(langchain_object).__name__}"
@ -115,23 +129,23 @@ def process_graph_cached(data_graph: Dict[str, Any], inputs: Optional[dict] = No
def load_flow_from_json(
input: Union[Path, str, dict], tweaks: Optional[dict] = None, build=True
flow: Union[Path, str, dict], tweaks: Optional[dict] = None, build=True
):
"""
Load flow from a JSON file or a JSON object.
:param input: JSON file path or JSON object
:param flow: JSON file path or JSON object
:param tweaks: Optional tweaks to be processed
:param build: If True, build the graph, otherwise return the graph object
:return: Langchain object or Graph object depending on the build parameter
"""
# If input is a file path, load JSON from the file
if isinstance(input, (str, Path)):
with open(input, "r", encoding="utf-8") as f:
if isinstance(flow, (str, Path)):
with open(flow, "r", encoding="utf-8") as f:
flow_graph = json.load(f)
# If input is a dictionary, assume it's a JSON object
elif isinstance(input, dict):
flow_graph = input
elif isinstance(flow, dict):
flow_graph = flow
else:
raise TypeError(
"Input must be either a file path (str) or a JSON object (dict)"

View file

@ -21,6 +21,7 @@ class Settings(BaseSettings):
toolkits: dict = {}
textsplitters: dict = {}
utilities: dict = {}
output_parsers: dict = {}
dev: bool = False
database_url: Optional[str] = None
cache: str = "InMemoryCache"
@ -66,7 +67,7 @@ class Settings(BaseSettings):
self.vectorstores = new_settings.vectorstores or {}
self.documentloaders = new_settings.documentloaders or {}
self.retrievers = new_settings.retrievers or {}
self.output_parsers = new_settings.output_parsers or {}
self.dev = dev
def update_settings(self, **kwargs):

View file

@ -21,6 +21,7 @@ class TemplateFieldCreator(BaseModel, ABC):
name: str = ""
display_name: Optional[str] = None
advanced: bool = False
input_types: list[str] = []
info: Optional[str] = ""
def to_dict(self):

View file

@ -13,6 +13,16 @@ NON_CHAT_AGENTS = {
}
class AgentFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
if field.name in ["suffix", "prefix"]:
field.show = True
if field.name == "Tools" and name == "ZeroShotAgent":
field.field_type = "BaseTool"
field.is_list = True
class SQLAgentNode(FrontendNode):
name: str = "SQLAgent"
template: Template = Template(

View file

@ -1,15 +1,16 @@
from collections import defaultdict
import re
from typing import List, Optional
from pydantic import BaseModel, Field
from langflow.template.frontend_node.formatter import field_formatters
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
from langflow.template.field.base import TemplateField
from langflow.template.template.base import Template
from langflow.utils import constants
from langflow.template.frontend_node.formatter import field_formatters
CLASSES_TO_REMOVE = ["Serializable", "BaseModel"]
CLASSES_TO_REMOVE = ["Serializable", "BaseModel", "object"]
class FieldFormatters(BaseModel):
@ -47,6 +48,8 @@ class FrontendNode(BaseModel):
name: str = ""
display_name: str = ""
documentation: str = ""
custom_fields: defaultdict = defaultdict(list)
output_types: List[str] = []
field_formatters: FieldFormatters = Field(default_factory=FieldFormatters)
def process_base_classes(self) -> None:
@ -76,6 +79,8 @@ class FrontendNode(BaseModel):
"description": self.description,
"base_classes": self.base_classes,
"display_name": self.display_name or self.name,
"custom_fields": self.custom_fields,
"output_types": self.output_types,
"documentation": self.documentation,
},
}

View file

@ -81,7 +81,7 @@ class ChainFrontendNode(FrontendNode):
field.advanced = False
if field.name == "verbose":
field.required = False
field.show = True
field.show = False
field.advanced = True
if field.name == "llm":
field.required = True

View file

@ -58,3 +58,7 @@ The base URL of the OpenAI API. Defaults to https://api.openai.com/v1.
You can change this to use other APIs like JinaChat, LocalAI and Prem.
"""
INPUT_KEY_INFO = """The variable to be used as Chat Input when more than one variable is available."""
OUTPUT_KEY_INFO = """The variable to be used as Chat Output (e.g. answer in a ConversationalRetrievalChain)"""

View file

@ -19,6 +19,10 @@ def build_file_field(
class DocumentLoaderFrontNode(FrontendNode):
def add_extra_base_classes(self) -> None:
self.base_classes = ["Document"]
self.output_types = ["Document"]
file_path_templates = {
"AirbyteJSONLoader": build_file_field(suffixes=[".json"], fileTypes=["json"]),
"CoNLLULoader": build_file_field(suffixes=[".csv"], fileTypes=["csv"]),

View file

@ -2,9 +2,10 @@ from abc import ABC, abstractmethod
from typing import Optional
from langflow.template.field.base import TemplateField
from pydantic import BaseModel
class FieldFormatter(ABC):
class FieldFormatter(BaseModel, ABC):
@abstractmethod
def format(self, field: TemplateField, name: Optional[str]) -> None:
pass

View file

@ -2,8 +2,13 @@ from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.frontend_node.constants import INPUT_KEY_INFO, OUTPUT_KEY_INFO
from langflow.template.template.base import Template
from langchain.memory.chat_message_histories.postgres import DEFAULT_CONNECTION_STRING
from langchain.memory.chat_message_histories.mongodb import (
DEFAULT_COLLECTION_NAME,
DEFAULT_DBNAME,
)
class MemoryFrontendNode(FrontendNode):
@ -66,11 +71,15 @@ class MemoryFrontendNode(FrontendNode):
field.required = False
field.show = True
field.advanced = False
if field.name in ["input_key", "output_key"]:
if field.name in {"input_key", "output_key"}:
field.required = False
field.show = True
field.advanced = False
field.value = ""
field.info = (
INPUT_KEY_INFO if field.name == "input_key" else OUTPUT_KEY_INFO
)
if field.name == "memory_key":
field.value = "chat_history"
if field.name == "chat_memory":
@ -80,9 +89,10 @@ class MemoryFrontendNode(FrontendNode):
if field.name == "url":
field.show = True
if field.name == "entity_store":
field.show = True
if name == "SQLiteEntityStore":
field.show = True
field.show = False
if name == "ConversationEntityMemory" and field.name == "memory_key":
field.show = False
field.required = False
class PostgresChatMessageHistoryFrontendNode(MemoryFrontendNode):
@ -120,3 +130,56 @@ class PostgresChatMessageHistoryFrontendNode(MemoryFrontendNode):
)
description: str = "Memory store with Postgres"
base_classes: list[str] = ["PostgresChatMessageHistory", "BaseChatMessageHistory"]
class MongoDBChatMessageHistoryFrontendNode(MemoryFrontendNode):
name: str = "MongoDBChatMessageHistory"
template: Template = Template(
# langchain/memory/chat_message_histories/mongodb.py
# connection_string: str,
# session_id: str,
# database_name: str = DEFAULT_DBNAME,
# collection_name: str = DEFAULT_COLLECTION_NAME,
type_name="MongoDBChatMessageHistory",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
name="session_id",
),
TemplateField(
field_type="str",
required=True,
show=True,
name="connection_string",
value="",
info="MongoDB connection string (e.g mongodb://mongo_user:password123@mongo:27017)",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value=DEFAULT_DBNAME,
name="database_name",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value=DEFAULT_COLLECTION_NAME,
name="collection_name",
),
],
)
description: str = "Memory store with MongoDB"
base_classes: list[str] = ["MongoDBChatMessageHistory", "BaseChatMessageHistory"]

View file

@ -0,0 +1,10 @@
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
class OutputParserFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
field.show = True

View file

@ -4,10 +4,14 @@ from langchain.text_splitter import Language
class TextSplittersFrontendNode(FrontendNode):
def add_extra_base_classes(self) -> None:
self.base_classes = ["Document"]
self.output_types = ["Document"]
def add_extra_fields(self) -> None:
self.template.add_field(
TemplateField(
field_type="BaseLoader",
field_type="Document",
required=True,
show=True,
name="documents",
@ -41,7 +45,7 @@ class TextSplittersFrontendNode(FrontendNode):
field_type="str",
required=True,
show=True,
value=".",
value="\\n",
name=name,
display_name="Separator",
)

View file

@ -53,7 +53,7 @@ class ToolNode(FrontendNode):
],
)
description: str = "Converts a chain, agent or function into a tool."
base_classes: list[str] = ["Tool"]
base_classes: list[str] = ["Tool", "BaseTool"]
def to_dict(self):
return super().to_dict()
@ -109,7 +109,7 @@ class PythonFunctionToolNode(FrontendNode):
],
)
description: str = "Python function to be executed."
base_classes: list[str] = ["Tool"]
base_classes: list[str] = ["BaseTool", "Tool"]
def to_dict(self):
return super().to_dict()

View file

@ -254,7 +254,7 @@ class VectorStoreFrontendNode(FrontendNode):
# when instantiating the vectorstores
field.name = "documents"
field.field_type = "TextSplitter"
field.field_type = "Document"
field.display_name = "Documents"
field.required = False
field.show = True

View file

@ -243,7 +243,11 @@ def format_dict(d, name: Optional[str] = None):
# Check for list type
if "List" in _type or "Sequence" in _type or "Set" in _type:
_type = _type.replace("List[", "")[:-1]
_type = (
_type.replace("List[", "")
.replace("Sequence[", "")
.replace("Set[", "")[:-1]
)
value["list"] = True
else:
value["list"] = False

View file

@ -5,6 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" />
<script src="/node_modules/ace-builds/src-min-noconflict/ace.js" type="text/javascript"></script>
<title>Langflow</title>
</head>
<body id='body' style="width: 100%; height:100%">

View file

@ -37,7 +37,7 @@
"base64-js": "^1.5.1",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"dompurify": "^3.0.3",
"dompurify": "^3.0.4",
"esbuild": "^0.17.18",
"lodash": "^4.17.21",
"lucide-react": "^0.233.0",
@ -85,6 +85,7 @@
"daisyui": "^3.1.1",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.2",
@ -146,33 +147,33 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz",
"integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
"integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz",
"integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==",
"version": "7.22.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.8.tgz",
"integrity": "sha512-75+KxFB4CZqYRXjx4NlR4J7yGvKumBuZTmV4NV6v09dVXXkuYVYLT68N6HCzLvfJ+fWCxQsntNzKwwIXL4bHnw==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.5",
"@babel/generator": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
"@babel/generator": "^7.22.7",
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helpers": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/helpers": "^7.22.6",
"@babel/parser": "^7.22.7",
"@babel/template": "^7.22.5",
"@babel/traverse": "^7.22.5",
"@babel/traverse": "^7.22.8",
"@babel/types": "^7.22.5",
"@nicolo-ribaudo/semver-v6": "^6.3.3",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.2",
"semver": "^6.3.0"
"json5": "^2.2.2"
},
"engines": {
"node": ">=6.9.0"
@ -194,9 +195,9 @@
}
},
"node_modules/@babel/generator": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz",
"integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==",
"version": "7.22.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.7.tgz",
"integrity": "sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ==",
"dependencies": {
"@babel/types": "^7.22.5",
"@jridgewell/gen-mapping": "^0.3.2",
@ -208,15 +209,15 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz",
"integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz",
"integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==",
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/compat-data": "^7.22.6",
"@babel/helper-validator-option": "^7.22.5",
"browserslist": "^4.21.3",
"lru-cache": "^5.1.1",
"semver": "^6.3.0"
"@nicolo-ribaudo/semver-v6": "^6.3.3",
"browserslist": "^4.21.9",
"lru-cache": "^5.1.1"
},
"engines": {
"node": ">=6.9.0"
@ -297,9 +298,9 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz",
"integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dependencies": {
"@babel/types": "^7.22.5"
},
@ -332,12 +333,12 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz",
"integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz",
"integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==",
"dependencies": {
"@babel/template": "^7.22.5",
"@babel/traverse": "^7.22.5",
"@babel/traverse": "^7.22.6",
"@babel/types": "^7.22.5"
},
"engines": {
@ -358,9 +359,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
"integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
"version": "7.22.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz",
"integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==",
"bin": {
"parser": "bin/babel-parser.js"
},
@ -404,17 +405,17 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz",
"integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==",
"version": "7.22.8",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz",
"integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==",
"dependencies": {
"@babel/code-frame": "^7.22.5",
"@babel/generator": "^7.22.5",
"@babel/generator": "^7.22.7",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.22.7",
"@babel/types": "^7.22.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
@ -1337,6 +1338,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/@nicolo-ribaudo/semver-v6": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
"integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2662,39 +2671,6 @@
"node": ">= 10"
}
},
"node_modules/@swc/cli/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/cli/node_modules/semver": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/cli/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/@swc/core": {
"version": "1.3.62",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.62.tgz",
@ -4124,14 +4100,14 @@
}
},
"node_modules/bin-version-check": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.0.0.tgz",
"integrity": "sha512-Q3FMQnS5eZmrBGqmDXLs4dbAn/f+52voP6ykJYmweSA60t6DyH4UTSwZhtbK5UH+LBoWvDljILUQMLRUtsynsA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz",
"integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==",
"dev": true,
"dependencies": {
"bin-version": "^6.0.0",
"semver": "^7.3.5",
"semver-truncate": "^2.0.0"
"semver": "^7.5.3",
"semver-truncate": "^3.0.0"
},
"engines": {
"node": ">=12"
@ -4140,39 +4116,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bin-version-check/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/bin-version-check/node_modules/semver": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/bin-version-check/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/bin-version/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -4306,9 +4249,9 @@
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
"node_modules/browserslist": {
"version": "4.21.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
"integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
"version": "4.21.9",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
"integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"funding": [
{
"type": "opencollective",
@ -4317,13 +4260,17 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001449",
"electron-to-chromium": "^1.4.284",
"node-releases": "^2.0.8",
"update-browserslist-db": "^1.0.10"
"caniuse-lite": "^1.0.30001503",
"electron-to-chromium": "^1.4.431",
"node-releases": "^2.0.12",
"update-browserslist-db": "^1.0.11"
},
"bin": {
"browserslist": "cli.js"
@ -4438,9 +4385,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001486",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz",
"integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==",
"version": "1.0.30001513",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz",
"integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww==",
"funding": [
{
"type": "opencollective",
@ -5142,9 +5089,9 @@
}
},
"node_modules/dompurify": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.4.tgz",
"integrity": "sha512-ae0mA+Qiqp6C29pqZX3fQgK+F91+F7wobM/v8DRzDqJdZJELXiFUx4PP4pK/mzUS0xkiSEx3Ncd9gr69jg3YsQ=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.440",
@ -8616,6 +8563,26 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.2.tgz",
"integrity": "sha512-e97lE6odGSiHonHJMTYC0q0iLXQyw0u5z/PJpvP/3vRy6/Zi9kLBwFAbEGjDzIowpjQv8b+J04PDamoUSQbzGA==",
"dev": true,
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"prettier": ">=2.0",
"typescript": ">=2.9"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"optional": true
}
}
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.3.0.tgz",
@ -9507,11 +9474,18 @@
}
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver-regex": {
@ -9527,17 +9501,38 @@
}
},
"node_modules/semver-truncate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-2.0.0.tgz",
"integrity": "sha512-Rh266MLDYNeML5h90ttdMwfXe1+Nc4LAWd9X1KdJe8pPHP4kFmvLZALtsMNHNdvTyQygbEC0D59sIz47DIaq8w==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz",
"integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==",
"dev": true,
"dependencies": {
"semver": "^6.0.0"
"semver": "^7.3.5"
},
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/semver/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/shadcn-ui": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/shadcn-ui/-/shadcn-ui-0.2.2.tgz",

View file

@ -32,7 +32,7 @@
"base64-js": "^1.5.1",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"dompurify": "^3.0.3",
"dompurify": "^3.0.4",
"esbuild": "^0.17.18",
"lodash": "^4.17.21",
"lucide-react": "^0.233.0",
@ -106,6 +106,7 @@
"daisyui": "^3.1.1",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.2",

View file

@ -1,20 +1,19 @@
import "reactflow/dist/style.css";
import { useState, useEffect, useContext } from "react";
import "./App.css";
import { useLocation } from "react-router-dom";
import _ from "lodash";
import { useContext, useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import "reactflow/dist/style.css";
import "./App.css";
import { ErrorBoundary } from "react-error-boundary";
import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
import CrashErrorComponent from "./components/CrashErrorComponent";
import Header from "./components/headerComponent";
import { alertContext } from "./contexts/alertContext";
import { locationContext } from "./contexts/locationContext";
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 } =
@ -118,7 +117,7 @@ export default function App() {
const removeAlert = (id: string) => {
setAlertsList((prevAlertsList) =>
prevAlertsList.filter((alert) => alert.id !== id),
prevAlertsList.filter((alert) => alert.id !== id)
);
};
@ -138,10 +137,7 @@ export default function App() {
<Router />
</ErrorBoundary>
<div></div>
<div
className="app-div"
style={{ zIndex: 999 }}
>
<div className="app-div" style={{ zIndex: 999 }}>
{alertsList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" ? (

View file

@ -1,31 +1,32 @@
import { Info } from "lucide-react";
import React, { useContext, useEffect, useRef, useState } from "react";
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import Dropdown from "../../../../components/dropdownComponent";
import FloatComponent from "../../../../components/floatComponent";
import InputComponent from "../../../../components/inputComponent";
import InputFileComponent from "../../../../components/inputFileComponent";
import InputListComponent from "../../../../components/inputListComponent";
import IntComponent from "../../../../components/intComponent";
import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { MAX_LENGTH_TO_SCROLL_TOOLTIP } from "../../../../constants";
import { PopUpContext } from "../../../../contexts/popUpContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components";
import { cleanEdges } from "../../../../util/reactflowUtils";
import {
classNames,
getRandomKeyByssmm,
groupByFamily,
isValidConnection,
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils";
import { useContext, useEffect, useRef, useState } from "react";
import InputComponent from "../../../../components/inputComponent";
import InputListComponent from "../../../../components/inputListComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components";
import FloatComponent from "../../../../components/floatComponent";
import Dropdown from "../../../../components/dropdownComponent";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import InputFileComponent from "../../../../components/inputFileComponent";
import { TabsContext } from "../../../../contexts/tabsContext";
import IntComponent from "../../../../components/intComponent";
import PromptAreaComponent from "../../../../components/promptComponent";
import { nodeNames } 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";
import { Info } from "lucide-react";
export default function ParameterComponent({
left,
@ -37,10 +38,12 @@ export default function ParameterComponent({
type,
name = "",
required = false,
optionalHandle = null,
info = "",
}: ParameterComponentType) {
const ref = useRef(null);
const refHtml = useRef(null);
const refNumberComponents = useRef(0);
const infoHtml = useRef(null);
const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0);
@ -58,10 +61,6 @@ export default function ParameterComponent({
updateNodeInternals(data.id);
}, [data.id, position, updateNodeInternals]);
const [enabled, setEnabled] = useState(
data.node.template[name]?.value ?? false
);
useEffect(() => {}, [closePopUp, data.node.template]);
const { reactFlowInstance } = useContext(typesContext);
@ -76,6 +75,7 @@ export default function ParameterComponent({
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
@ -95,44 +95,55 @@ export default function ParameterComponent({
}, [info]);
useEffect(() => {
const groupedObj = groupByFamily(myData, tooltipTitle);
const groupedObj = groupByFamily(myData, tooltipTitle, left, data.type);
refHtml.current = groupedObj.map((item, i) => (
<span
key={getRandomKeyByssmm()}
className={classNames(
i > 0 ? "mt-3 flex items-center" : "flex items-center"
)}
>
<div
className="h-6 w-6"
style={{
color: nodeColors[item.family],
}}
refNumberComponents.current = groupedObj[0]?.type?.length;
refHtml.current = groupedObj.map((item, i) => {
const Icon: any = nodeIconsLucide[item.family];
return (
<span
key={getRandomKeyByssmm() + item.family + i}
className={classNames(
i > 0 ? "mt-2 flex items-center" : "flex items-center"
)}
>
{React.createElement(nodeIconsLucide[item.family])}
</div>
<span className="ps-2 text-foreground">
{nodeNames[item.family] ?? ""}{" "}
<span className={classNames(left ? "hidden" : "")}>
{" "}
-&nbsp;
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, i) => (
<React.Fragment key={el + i}>
<span>
{i === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
{i % 2 === 0 && i > 0 && <br />}
</React.Fragment>
))
: item.type}
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? ""}{" "}
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, i) => (
<React.Fragment key={el + i}>
<span>
{i === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
</span>
</span>
</span>
));
);
});
}, [tooltipTitle]);
return (
@ -149,7 +160,7 @@ export default function ParameterComponent({
}
>
{title}
<span className="text-destructive">{required ? " *" : ""}</span>
<span className="text-status-red">{required ? " *" : ""}</span>
<div className="">
{info !== "" && (
<ShadTooltip content={infoHtml.current}>
@ -165,10 +176,16 @@ export default function ParameterComponent({
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int") ? (
type === "int") &&
!optionalHandle ? (
<></>
) : (
<ShadTooltip
styleClasses={
refNumberComponents.current > MAX_LENGTH_TO_SCROLL_TOOLTIP
? "tooltip-fixed-width custom-scroll overflow-y-scroll nowheel"
: "tooltip-fixed-width"
}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
@ -227,10 +244,9 @@ export default function ParameterComponent({
<div className="mt-2 w-full">
<ToggleShadComponent
disabled={disabled}
enabled={enabled}
enabled={data.node.template[name].value}
setEnabled={(t) => {
handleOnNewValue(t);
setEnabled(t);
}}
size="large"
/>
@ -257,6 +273,10 @@ export default function ParameterComponent({
) : left === true && type === "code" ? (
<div className="mt-2 w-full">
<CodeAreaComponent
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
nodeClass={data.node}
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
@ -288,6 +308,20 @@ export default function ParameterComponent({
) : left === true && type === "prompt" ? (
<div className="mt-2 w-full">
<PromptAreaComponent
field_name={name}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
if (reactFlowInstance) {
cleanEdges({
flow: {
edges: reactFlowInstance.getEdges(),
nodes: reactFlowInstance.getNodes(),
},
updateEdge: (edge) => reactFlowInstance.setEdges(edge),
});
}
}}
nodeClass={data.node}
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}

View file

@ -1,3 +1,15 @@
import { Zap } from "lucide-react";
import { useContext, useEffect, useRef, useState } from "react";
import { NodeToolbar } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent";
import { useSSE } from "../../contexts/SSEContext";
import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { typesContext } from "../../contexts/typesContext";
import NodeModal from "../../modals/NodeModal";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { NodeDataType } from "../../types/flow";
import {
classNames,
nodeColors,
@ -5,17 +17,6 @@ import {
toTitleCase,
} from "../../utils";
import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext";
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 Tooltip from "../../components/TooltipComponent";
import { NodeToolbar } from "reactflow";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import { useSSE } from "../../contexts/SSEContext";
export default function GenericNode({
data,
@ -66,9 +67,7 @@ export default function GenericNode({
deleteNode(data.id);
return;
}
useEffect(() => {}, [closePopUp, data.node.template]);
return (
<>
<NodeToolbar>
@ -95,10 +94,7 @@ export default function GenericNode({
}}
/>
<div className="generic-node-tooltip-div">
<ShadTooltip
delayDuration={1500}
content={data.node.display_name}
>
<ShadTooltip content={data.node.display_name}>
<div className="generic-node-tooltip-div text-primary">
{data.node.display_name}
</div>
@ -118,14 +114,24 @@ export default function GenericNode({
<div>
<Tooltip
title={
!validationStatus ? (
"Validating..."
isBuilding ? (
<span>Building...</span>
) : !validationStatus ? (
<span className="flex">
Build{" "}
<Zap
className="mx-0.5 h-5 fill-build-trigger stroke-build-trigger stroke-1"
strokeWidth={1.5}
/>{" "}
flow to validate status.
</span>
) : (
<div className="generic-node-validation-div">
{validationStatus.params ||
""
.split("\n")
.map((line, index) => <div key={index}>{line}</div>)}
<div className="max-h-96 overflow-auto">
{validationStatus.params
? validationStatus.params
.split("\n")
.map((line, index) => <div key={index}>{line}</div>)
: ""}
</div>
)
}
@ -162,9 +168,7 @@ export default function GenericNode({
</div>
<div className="generic-node-desc">
<div className="generic-node-desc-text">
{data.node.description}
</div>
<div className="generic-node-desc-text">{data.node.description}</div>
<>
{Object.keys(data.node.template)
@ -196,6 +200,7 @@ export default function GenericNode({
data={data}
color={
nodeColors[types[data.node.template[t].type]] ??
nodeColors[data.node.template[t].type] ??
nodeColors.unknown
}
title={
@ -207,11 +212,22 @@ export default function GenericNode({
}
info={data.node.template[t].info}
name={t}
tooltipTitle={data.node.template[t].type}
tooltipTitle={
data.node.template[t].input_types?.join("\n") ??
data.node.template[t].type
}
required={data.node.template[t].required}
id={data.node.template[t].type + "|" + t + "|" + data.id}
id={
(data.node.template[t].input_types?.join(";") ??
data.node.template[t].type) +
"|" +
t +
"|" +
data.id
}
left={true}
type={data.node.template[t].type}
optionalHandle={data.node.template[t].input_types}
/>
) : (
<></>
@ -232,8 +248,12 @@ export default function GenericNode({
<ParameterComponent
data={data}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={data.type}
tooltipTitle={`${data.node.base_classes.join("\n")}`}
title={
data.node.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node.base_classes.join("\n")}
id={[data.type, data.id, ...data.node.base_classes].join("|")}
type={data.node.base_classes.join("|")}
left={false}

View file

@ -1,8 +1,8 @@
import { Link } from "react-router-dom";
import { Transition } from "@headlessui/react";
import { CheckCircle2, Info, X, XCircle } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { SingleAlertComponentType } from "../../../../types/alerts";
import { X, CheckCircle2, Info, XCircle } from "lucide-react";
export default function SingleAlert({
dropItem,

View file

@ -1,10 +1,10 @@
import { useContext, useEffect, useRef } from "react";
import { Trash2, X } from "lucide-react";
import { useContext, useRef } from "react";
import { alertContext } from "../../contexts/alertContext";
import SingleAlert from "./components/singleAlertComponent";
import { AlertDropdownType } from "../../types/alerts";
import { PopUpContext } from "../../contexts/popUpContext";
import { AlertDropdownType } from "../../types/alerts";
import { useOnClickOutside } from "../hooks/useOnClickOutside";
import { X, Trash2 } from "lucide-react";
import SingleAlert from "./components/singleAlertComponent";
export default function AlertDropdown({}: AlertDropdownType) {
const { closePopUp } = useContext(PopUpContext);

View file

@ -1,7 +1,7 @@
import { Transition } from "@headlessui/react";
import { XCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { ErrorAlertType } from "../../types/alerts";
import { XCircle } from "lucide-react";
export default function ErrorAlert({
title,
@ -20,6 +20,7 @@ export default function ErrorAlert({
}, 5000);
}
}, [id, removeAlert, show]);
return (
<Transition
className="relative"
@ -43,13 +44,15 @@ export default function ErrorAlert({
>
<div className="flex">
<div className="flex-shrink-0">
<XCircle className="error-build-message-circle" aria-hidden="true" />
<XCircle
className="error-build-message-circle"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="error-build-foreground">
{title}
</h3>
{list.length !== 0 ? (
<h3 className="error-build-foreground">{title}</h3>
{list?.length !== 0 &&
list?.some((item) => item !== null && item !== undefined) ? (
<div className="error-build-message-div">
<ul className="error-build-message-list">
{list.map((item, index) => (

View file

@ -1,8 +1,8 @@
import { Transition } from "@headlessui/react";
import { Info } from "lucide-react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { NoticeAlertType } from "../../types/alerts";
import { Info } from "lucide-react";
export default function NoticeAlert({
title,

View file

@ -1,7 +1,7 @@
import { Transition } from "@headlessui/react";
import { CheckCircle2 } from "lucide-react";
import { useEffect, useState } from "react";
import { SuccessAlertType } from "../../types/alerts";
import { CheckCircle2 } from "lucide-react";
export default function SuccessAlert({
title,
@ -38,15 +38,10 @@ export default function SuccessAlert({
>
<div className="flex">
<div className="flex-shrink-0">
<CheckCircle2
className="success-alert-icon"
aria-hidden="true"
/>
<CheckCircle2 className="success-alert-icon" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="success-alert-message">
{title}
</p>
<p className="success-alert-message">{title}</p>
</div>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,16 +1,11 @@
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
import {
AccordionComponentType,
ProgressBarType,
} from "../../types/components";
import { Progress } from "../../components/ui/progress";
import { setInterval } from "timers/promises";
import { useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../../components/ui/accordion";
import { AccordionComponentType } from "../../types/components";
export default function AccordionComponent({
trigger,
@ -18,7 +13,7 @@ export default function AccordionComponent({
open = [],
}: AccordionComponentType) {
const [value, setValue] = useState(
open.length == 0 ? "" : getOpenAccordion(),
open.length === 0 ? "" : getOpenAccordion()
);
function getOpenAccordion() {

View file

@ -1,7 +1,7 @@
import React, { useState, ChangeEvent } from "react";
import { Textarea } from "../../components/ui/textarea";
import { Label } from "../../components/ui/label";
import React, { ChangeEvent, useState } from "react";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea";
type InputProps = {
name: string | null;
@ -47,9 +47,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
<div className="edit-flow-arrangement">
<span className="font-medium">Name</span>{" "}
{isMaxLength && (
<span className="edit-flow-span">
Character limit reached
</span>
<span className="edit-flow-span">Character limit reached</span>
)}
</div>
<Input

View file

@ -1,8 +1,8 @@
import { Disclosure } from "@headlessui/react";
import { useContext } from "react";
import { Link } from "react-router-dom";
import { classNames } from "../../utils";
import { locationContext } from "../../contexts/locationContext";
import { classNames } from "../../utils";
export default function ExtraSidebar() {
const {
@ -16,9 +16,7 @@ export default function ExtraSidebar() {
return (
<>
<aside
className={` ${
isStackedOpen ? "w-52" : "w-0 "
} unused-side-bar-aside`}
className={` ${isStackedOpen ? "w-52" : "w-0 "} unused-side-bar-aside`}
>
<div className="unused-side-bar-arrangement">
<div className="unused-side-bar-division">
@ -71,7 +69,9 @@ export default function ExtraSidebar() {
<span className="flex-1">{item.name}</span>
<svg
className={classNames(
open ? "unused-side-bar-svg-true" : "text-ring",
open
? "unused-side-bar-svg-true"
: "text-ring",
"unused-side-bar-svg"
)}
viewBox="0 0 20 20"

View file

@ -1,5 +1,5 @@
import { styled } from "@mui/material/styles";
import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip";
import { styled } from "@mui/material/styles";
export const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />

View file

@ -1,6 +1,3 @@
import { useContext, useEffect, useRef, useState } from "react";
import { RadialProgressType } from "../../types/components";
export default function LoadingSpinner({}) {
return <></>;
}

View file

@ -1,21 +0,0 @@
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
import { ProgressBarType } from "../../types/components";
import { Progress } from "../../components/ui/progress";
import { progressContext } from "../../contexts/ProgressContext";
import { setInterval } from "timers/promises";
export default function ProgressBarComponent({
value,
children,
}: ProgressBarType) {
const ref = useRef(0);
const reff = useRef();
const { progress } = useContext(progressContext);
useEffect(() => {
ref.current = progress * 100;
console.log(progress);
}, [progress]);
return <Progress className="h-2.5" value={ref.current} />;
}

View file

@ -1,4 +1,3 @@
import { useContext, useEffect, useRef, useState } from "react";
import { RadialProgressType } from "../../types/components";
export default function RadialProgressComponent({

View file

@ -0,0 +1,21 @@
import DOMPurify from "dompurify";
const SanitizedHTMLWrapper = ({
className,
content,
onClick,
suppressWarning = false,
}) => {
const sanitizedHTML = DOMPurify.sanitize(content);
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
suppressContentEditableWarning={suppressWarning}
onClick={onClick}
/>
);
};
export default SanitizedHTMLWrapper;

View file

@ -1,28 +1,26 @@
import { ShadTooltipProps } from "../../types/components";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { ShadToolTipType } from "../../types/components";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
const ShadTooltip = ({
delayDuration = 500,
side,
export default function ShadTooltip({
content,
side,
asChild = true,
children,
}: ShadTooltipProps) => {
styleClasses,
delayDuration = 500,
}: ShadToolTipType) {
return (
<TooltipProvider>
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent side={side} avoidCollisions={false} sticky="always">
{content}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipContent
className={styleClasses}
side={side}
avoidCollisions={false}
sticky="always"
>
{content}
</TooltipContent>
</Tooltip>
);
};
export default ShadTooltip;
}

View file

@ -1,6 +1,5 @@
import { ReactElement } from "react";
import { LightTooltip } from "../LightTooltipComponent";
import { TooltipComponentType } from "../../types/components";
import { LightTooltip } from "../LightTooltipComponent";
export default function Tooltip({
children,

View file

@ -1,15 +1,14 @@
import { Trash2, ExternalLink } from "lucide-react";
import { Trash2 } 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,
Card,
CardDescription,
CardFooter,
Card,
CardHeader,
CardTitle,
} from "../ui/card";
export const CardComponent = ({
@ -35,9 +34,7 @@ export const CardComponent = ({
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length]
}
></span>
<span className="card-component-title-size">
{flow.name}
</span>
<span className="card-component-title-size">{flow.name}</span>
{onDelete && (
<button className="card-component-delete-button" onClick={onDelete}>
<Trash2 className="card-component-delete-icon" />

View file

@ -1,22 +1,21 @@
import { useContext, useState } from "react";
import { Transition } from "@headlessui/react";
import { Zap } from "lucide-react";
import { validateNodes } from "../../../utils";
import { FlowType } from "../../../types/flow";
import { useContext, useState } from "react";
import Loading from "../../../components/ui/loading";
import { useSSE } from "../../../contexts/SSEContext";
import { typesContext } from "../../../contexts/typesContext";
import { alertContext } from "../../../contexts/alertContext";
import { typesContext } from "../../../contexts/typesContext";
import { postBuildInit } from "../../../controllers/API";
import ShadTooltip from "../../ShadTooltipComponent";
import { FlowType } from "../../../types/flow";
import { validateNodes } from "../../../utils";
import { TabsContext } from "../../../contexts/tabsContext";
import RadialProgressComponent from "../../RadialProgress";
export default function BuildTrigger({
open,
flow,
setIsBuilt,
isBuilt,
}: {
open: boolean;
flow: FlowType;
@ -25,6 +24,7 @@ export default function BuildTrigger({
}) {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(TabsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : "";
@ -88,6 +88,16 @@ export default function BuildTrigger({
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys) {
setTabsState((old) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
@ -127,7 +137,7 @@ export default function BuildTrigger({
async function enforceMinimumLoadingTime(
startTime: number,
minimumLoadingTime: number,
minimumLoadingTime: number
) {
const elapsedTime = Date.now() - startTime;
const remainingTime = minimumLoadingTime - elapsedTime;
@ -156,11 +166,7 @@ export default function BuildTrigger({
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div
className={
"round-buttons-position" + (isBuilt ? " bottom-20" : " bottom-4")
}
>
<div className="fixed bottom-20 right-4">
<div
className={`${eventClick} round-button-form`}
onClick={() => {
@ -184,7 +190,10 @@ export default function BuildTrigger({
className="build-trigger-loading-icon"
/>
) : (
<Zap strokeWidth={1.5} className="build-trigger-icon" />
<Zap
strokeWidth={1.5}
className="sh-6 w-6 fill-build-trigger stroke-build-trigger stroke-1"
/>
)}
</div>
</button>

View file

@ -1,55 +0,0 @@
import { useState } from "react";
import { ChatMessageType } from "../../../types/chat";
import { nodeColors } from "../../../utils";
import Convert from "ansi-to-html";
import { MessageCircle } from "lucide-react";
import DOMPurify from "dompurify";
const convert = new Convert({ newline: true });
export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
const [hidden, setHidden] = useState(true);
return (
<div>
{!chat.isSend ? (
<div className="w-full text-start">
<div
style={{ backgroundColor: nodeColors["chat"] }}
className=" relative inline-block w-fit max-w-[280px] overflow-hidden rounded-xl rounded-tl-none text-start text-sm font-normal text-background"
>
{hidden && chat.thought && chat.thought !== "" && (
<div
onClick={() => setHidden((prev) => !prev)}
className="absolute right-2 top-2 cursor-pointer"
>
<MessageCircle className="h-5 w-5 animate-bounce" />
</div>
)}
{chat.thought && chat.thought !== "" && !hidden && (
<div
onClick={() => setHidden((prev) => !prev)}
style={{ backgroundColor: nodeColors["thought"] }}
className=" inline-block w-full cursor-pointer px-5 pb-3 pt-3 text-start"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(convert.toHtml(chat.thought)),
}}
></div>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div
className="w-full rounded-b-md px-4 pb-3 pr-8 pt-3"
style={{ backgroundColor: nodeColors["chat"] }}
>
{chat.message}
</div>
</div>
</div>
) : (
<div className="w-full text-end">
<div className="inline-block w-fit max-w-[280px] overflow-hidden rounded-xl rounded-tr-none bg-input p-3 px-5 text-start text-sm font-normal text-black">
{chat.message}
</div>
</div>
)}
</div>
);
}

View file

@ -1,20 +1,32 @@
import { Transition } from "@headlessui/react";
import { MessagesSquare } from "lucide-react";
import { alertContext } from "../../../contexts/alertContext";
import { useContext } from "react";
import ShadTooltip from "../../ShadTooltipComponent";
import {
CHAT_CANNOT_OPEN_DESCRIPTION,
CHAT_CANNOT_OPEN_TITLE,
FLOW_NOT_BUILT_DESCRIPTION,
FLOW_NOT_BUILT_TITLE,
} from "../../../constants";
import { alertContext } from "../../../contexts/alertContext";
export default function ChatTrigger({ open, setOpen, isBuilt }) {
export default function ChatTrigger({ open, setOpen, isBuilt, canOpen }) {
const { setErrorData } = useContext(alertContext);
function handleClick() {
if (isBuilt) {
setOpen(true);
if (canOpen) {
setOpen(true);
} else {
setErrorData({
title: CHAT_CANNOT_OPEN_TITLE,
list: [CHAT_CANNOT_OPEN_DESCRIPTION],
});
}
} else {
setErrorData({
title: "Flow not built",
list: ["Please build the flow before chatting"],
title: FLOW_NOT_BUILT_TITLE,
list: [FLOW_NOT_BUILT_DESCRIPTION],
});
}
}
@ -30,19 +42,26 @@ export default function ChatTrigger({ open, setOpen, isBuilt }) {
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="message-button-position">
<div className="round-button-form" onClick={handleClick}>
<button>
<div className="round-button-div">
<MessagesSquare
className="message-button-icon"
style={{ color: "white" }}
strokeWidth={1.5}
/>
</div>
</button>
<button
onClick={handleClick}
className={
"shadow-round-btn-shadow hover:shadow-round-btn-shadow message-button " +
(!isBuilt || !canOpen ? "cursor-not-allowed" : "cursor-pointer")
}
>
<div className="flex gap-3">
<MessagesSquare
className={
"h-6 w-6 transition-all " +
(isBuilt && canOpen
? "message-button-icon"
: "disabled-message-button-icon")
}
style={{ color: "white" }}
strokeWidth={1.5}
/>
</div>
</div>
</button>
</Transition>
);
}

View file

@ -1,16 +1,20 @@
import { useEffect, useRef, useState } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import { useNodes } from "reactflow";
import { ChatType } from "../../types/chat";
import ChatTrigger from "./chatTrigger";
import BuildTrigger from "./buildTrigger";
import ChatModal from "../../modals/chatModal";
import ChatTrigger from "./chatTrigger";
import * as _ from "lodash";
import { TabsContext } from "../../contexts/tabsContext";
import { getBuildStatus } from "../../controllers/API";
import FormModal from "../../modals/formModal";
import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType) {
const [open, setOpen] = useState(false);
const [isBuilt, setIsBuilt] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { tabsState } = useContext(TabsContext);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@ -44,41 +48,59 @@ export default function Chat({ flow }: ChatType) {
const nodes = useNodes();
useEffect(() => {
const prevNodes = prevNodesRef.current;
const currentNodes = nodes.map(
(node: NodeType) => node.data.node.template.value,
const currentNodes = nodes.map((node: NodeType) =>
_.cloneDeep(node.data.node.template)
);
if (
prevNodes &&
tabsState &&
tabsState[flow.id] &&
tabsState[flow.id].isPending &&
JSON.stringify(prevNodes) !== JSON.stringify(currentNodes)
) {
setIsBuilt(false);
}
if (
tabsState &&
tabsState[flow.id] &&
tabsState[flow.id].formKeysData &&
tabsState[flow.id].formKeysData.input_keys &&
Object.keys(tabsState[flow.id].formKeysData.input_keys).length > 0
) {
setCanOpen(true);
} else {
setCanOpen(false);
}
prevNodesRef.current = currentNodes;
}, [nodes]);
}, [tabsState, flow.id, nodes]);
return (
<>
{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>
) : (
<div>
<BuildTrigger
open={open}
flow={flow}
setIsBuilt={setIsBuilt}
isBuilt={isBuilt}
/>
)}
{isBuilt &&
tabsState[flow.id] &&
tabsState[flow.id].formKeysData &&
canOpen && (
<FormModal
key={flow.id}
flow={flow}
open={open}
setOpen={setOpen}
/>
)}
<ChatTrigger
canOpen={canOpen}
open={open}
setOpen={setOpen}
isBuilt={isBuilt}
/>
</div>
</>
);
}

View file

@ -1,7 +1,6 @@
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 { ExternalLink } from "lucide-react";
@ -11,9 +10,11 @@ export default function CodeAreaComponent({
onChange,
disabled,
editNode = false,
nodeClass,
setNodeClass,
}: TextAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value),
typeof value == "string" ? value : JSON.stringify(value)
);
const { openPopUp } = useContext(PopUpContext);
useEffect(() => {
@ -28,29 +29,27 @@ export default function CodeAreaComponent({
}, [value]);
return (
<div
className={
disabled ? "code-area-component" : "w-full"
}
>
<div className="code-area-input-positioning">
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<div className="flex w-full items-center">
<span
onClick={() => {
openPopUp(
<CodeAreaModal
value={myValue}
nodeClass={nodeClass}
setNodeClass={setNodeClass}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>,
/>
);
}}
className={
editNode
? "input-edit-node input-dialog"
: "input-dialog input-primary " +
(disabled ? "input-disable" : "")
: (disabled ? " input-disable input-ring " : "") +
" input-primary text-muted-foreground "
}
>
{myValue !== "" ? myValue : "Type something..."}
@ -59,17 +58,25 @@ export default function CodeAreaComponent({
onClick={() => {
openPopUp(
<CodeAreaModal
setNodeClass={setNodeClass}
value={myValue}
nodeClass={nodeClass}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>,
/>
);
}}
>
{!editNode && (
<ExternalLink strokeWidth={1.5} className="code-area-external-link" />
<ExternalLink
strokeWidth={1.5}
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</button>
</div>

View file

@ -1,9 +1,9 @@
import { Listbox, Transition } from "@headlessui/react";
import { Check, ChevronsUpDown } from "lucide-react";
import { Fragment, useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { DropDownComponentType } from "../../types/components";
import { classNames } from "../../utils";
import { ChevronsUpDown, Check } from "lucide-react";
import { PopUpContext } from "../../contexts/popUpContext";
export default function Dropdown({
value,
@ -45,11 +45,7 @@ export default function Dropdown({
<span className="dropdown-component-display">
{internalValue}
</span>
<span
className={
"dropdown-component-arrow"
}
>
<span className={"dropdown-component-arrow"}>
<ChevronsUpDown
className="dropdown-component-arrow-color"
aria-hidden="true"
@ -67,8 +63,8 @@ export default function Dropdown({
<Listbox.Options
className={classNames(
editNode
? "dropdown-component-true-options "
: "dropdown-component-false-options ",
? "dropdown-component-true-options nowheel custom-scroll"
: "dropdown-component-false-options nowheel custom-scroll",
apiModal ? "mb-2 w-[250px]" : "absolute"
)}
>

View file

@ -1,7 +1,7 @@
import { useContext, useEffect, useState } from "react";
import { FloatComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FloatComponentType } from "../../types/components";
export default function FloatComponent({
value,
@ -30,11 +30,7 @@ export default function FloatComponent({
}, [closePopUp]);
return (
<div
className={
"w-full " + (disabled ? "float-component-pointer" : "")
}
>
<div className={"w-full " + (disabled ? "float-component-pointer" : "")}>
<input
onFocus={() => {
if (disableCopyPaste) setDisableCopyPaste(true);

View file

@ -1,27 +1,24 @@
import { useContext } from "react";
import { TabsContext } from "../../../../contexts/tabsContext";
import { PopUpContext } from "../../../../contexts/popUpContext";
import {
Plus,
ChevronDown,
ChevronLeft,
Undo,
Plus,
Redo,
Settings2,
Undo,
} from "lucide-react";
import { useContext } from "react";
import { PopUpContext } from "../../../../contexts/popUpContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../../../ui/dropdown-menu";
import { alertContext } from "../../../../contexts/alertContext";
import { Link, useNavigate } from "react-router-dom";
import { alertContext } from "../../../../contexts/alertContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import { Button } from "../../../ui/button";
@ -54,13 +51,11 @@ export const MenuBar = ({ flows, tabId }) => {
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="header-menu-bar-display"
variant="primary"
size="sm"
>
<div className="header-menu-flow-name">{current_flow.name}</div>
<ChevronDown className="h-4 w-4" />
<Button asChild variant="primary" size="sm">
<div className="header-menu-bar-display">
<div className="header-menu-flow-name">{current_flow.name}</div>
<ChevronDown className="h-4 w-4" />
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-44">
@ -101,7 +96,7 @@ export const MenuBar = ({ flows, tabId }) => {
<Redo className="header-menu-options " />
Redo
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* <DropdownMenuSeparator /> */}
{/* <DropdownMenuLabel>Projects</DropdownMenuLabel> */}
{/* <DropdownMenuRadioGroup className="max-h-full overflow-scroll"
value={tabId}

View file

@ -1,19 +1,18 @@
import { Home, MoonIcon, SunIcon, Users2 } from "lucide-react";
import { Bell, Home, MoonIcon, SunIcon, Users2 } from "lucide-react";
import { useContext, useEffect, useState } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { Button } from "../ui/button";
import { TabsContext } from "../../contexts/tabsContext";
import { Link, useLocation, useParams } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants";
import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext";
import MenuBar from "./components/menuBar";
import { Link, useLocation, useParams } from "react-router-dom";
import { USER_PROJECTS_HEADER } from "../../constants";
import { getRepoStars } from "../../controllers/API";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import { Bell } from "lucide-react";
import MenuBar from "./components/menuBar";
export default function Header() {
const { flows, addFlow, tabId } = useContext(TabsContext);
@ -79,9 +78,7 @@ export default function Header() {
>
<FaGithub className="mr-2 h-5 w-5" />
Star
<div className="header-github-display">
{stars}
</div>
<div className="header-github-display">{stars}</div>
</a>
<a
href="https://twitter.com/logspace_ai"
@ -133,9 +130,7 @@ export default function Header() {
);
}}
>
{notificationCenter && (
<div className="header-notifications"></div>
)}
{notificationCenter && <div className="header-notifications"></div>}
<Bell className="side-bar-button-size" aria-hidden="true" />
</button>
</div>

View file

@ -1,8 +1,8 @@
import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { TabsContext } from "../../contexts/tabsContext";
import { InputComponentType } from "../../types/components";
import { classNames } from "../../utils";
import { TabsContext } from "../../contexts/tabsContext";
import { PopUpContext } from "../../contexts/popUpContext";
export default function InputComponent({
value,
@ -29,13 +29,7 @@ export default function InputComponent({
}, [closePopUp]);
return (
<div
className={
disabled
? "input-component-div"
: "relative"
}
>
<div className={disabled ? "input-component-div" : "relative"}>
<input
value={myValue}
onFocus={() => {
@ -45,11 +39,13 @@ export default function InputComponent({
if (disableCopyPaste) setDisableCopyPaste(false);
}}
className={classNames(
" pr-9 ",
disabled ? " input-disable " : "",
password && !pwdVisible && myValue !== "" ? "password" : "",
password && !pwdVisible && myValue !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : " input-primary ",
password && editNode ? "pr-8" : "pr-3"
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : ""
)}
placeholder={password && editNode ? "Key" : "Type something..."}
onChange={(e) => {

View file

@ -1,9 +1,9 @@
import { FileSearch2 } from "lucide-react";
import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
import { FileComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
import { FileSearch2 } from "lucide-react";
import { uploadFile } from "../../controllers/API";
import { FileComponentType } from "../../types/components";
export default function InputFileComponent({
value,
@ -92,18 +92,16 @@ export default function InputFileComponent({
};
return (
<div
className={
disabled ? "input-component-div" : "w-full"
}
>
<div className={disabled ? "input-component-div" : "w-full"}>
<div className="input-file-component">
<span
onClick={handleButtonClick}
className={
editNode
? "input-edit-node " + "input-primary "
: "input-primary " + (disabled ? "input-disable " : "")
? "input-edit-node input-dialog text-muted-foreground"
: disabled
? "input-disable input-dialog input-primary"
: "input-dialog input-primary text-muted-foreground"
}
>
{myValue !== "" ? myValue : "No file"}
@ -112,7 +110,10 @@ export default function InputFileComponent({
{!editNode && !loading && (
<FileSearch2
strokeWidth={1.5}
className="h-6 w-6 hover:text-accent-foreground"
className={
"icons-parameters-comp" +
(disabled ? " text-ring " : " hover:text-accent-foreground")
}
/>
)}
{!editNode && loading && (

View file

@ -1,9 +1,8 @@
import { useContext, useEffect, useState } from "react";
import { InputListComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
import _ from "lodash";
import { X, Plus } from "lucide-react";
import { Plus, X } from "lucide-react";
import { PopUpContext } from "../../contexts/popUpContext";
export default function InputListComponent({

View file

@ -1,8 +1,7 @@
import { useContext, useEffect, useState } from "react";
import { FloatComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
import { classNames } from "../../utils";
import { PopUpContext } from "../../contexts/popUpContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FloatComponentType } from "../../types/components";
export default function IntComponent({
value,
@ -31,7 +30,7 @@ export default function IntComponent({
<div
className={
"w-full " +
(disabled ? "pointer-events-none w-full cursor-not-allowed" : "w-full")
(disabled ? "pointer-events-none w-full cursor-not-allowed" : "")
}
>
<input
@ -42,7 +41,6 @@ export default function IntComponent({
if (disableCopyPaste) setDisableCopyPaste(false);
}}
onKeyDown={(event) => {
// console.log(event);
if (
event.key !== "Backspace" &&
event.key !== "Enter" &&
@ -72,7 +70,7 @@ export default function IntComponent({
className={
editNode
? " input-edit-node "
: " input-primary " + (disabled ? " input-disable " : "")
: " input-primary " + (disabled ? " input-disable" : "")
}
placeholder={editNode ? "Integer number" : "Type an integer number"}
onChange={(e) => {

View file

@ -1,12 +1,17 @@
import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
import { TypeModal } from "../../utils";
import { ExternalLink } from "lucide-react";
import { typesContext } from "../../contexts/typesContext";
import { postValidatePrompt } from "../../controllers/API";
export default function PromptAreaComponent({
field_name,
setNodeClass,
nodeClass,
value,
onChange,
disabled,
@ -14,6 +19,7 @@ export default function PromptAreaComponent({
}: TextAreaComponentType) {
const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext);
const { reactFlowInstance } = useContext(typesContext);
useEffect(() => {
if (disabled) {
setMyValue("");
@ -23,14 +29,36 @@ export default function PromptAreaComponent({
useEffect(() => {
setMyValue(value);
}, [value]);
if (value !== "" && !editNode) {
postValidatePrompt(field_name, value, nodeClass).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass(apiReturn.data.frontend_node);
// need to update reactFlowInstance to re-render the nodes.
}
});
}
}, [value, reactFlowInstance]);
// useEffect(() => {
// if (value !== "" && myValue !== value && reactFlowInstance) {
// // only executed once
// setMyValue(value);
// postValidatePrompt(field_name, value, nodeClass)
// .then((apiReturn) => {
// if (apiReturn.data) {
// setNodeClass(apiReturn.data.frontend_node);
// // need to update reactFlowInstance to re-render the nodes.
// reactFlowInstance.setEdges(
// _.cloneDeep(reactFlowInstance.getEdges())
// );
// }
// })
// .catch((error) => {});
// }
// }, [reactFlowInstance, field_name, myValue, nodeClass, setNodeClass, value]);
return (
<div
className={
disabled ? "pointer-events-none w-full cursor-not-allowed" : " w-full"
}
>
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<div className="flex w-full items-center">
<span
onClick={() => {
@ -44,15 +72,16 @@ export default function PromptAreaComponent({
setMyValue(t);
onChange(t);
}}
/>,
nodeClass={nodeClass}
setNodeClass={setNodeClass}
/>
);
}}
className={
editNode
? " input-edit-node " + " input-dialog "
: (disabled ? " input-disable " : "") +
" input-primary " +
" input-dialog "
? "input-edit-node input-dialog"
: (disabled ? " input-disable text-ring " : "") +
" input-primary text-muted-foreground "
}
>
{myValue !== "" ? myValue : "Type your prompt here"}
@ -61,6 +90,7 @@ export default function PromptAreaComponent({
onClick={() => {
openPopUp(
<GenericModal
field_name={field_name}
type={TypeModal.PROMPT}
value={myValue}
buttonText="Check & Save"
@ -69,14 +99,19 @@ export default function PromptAreaComponent({
setMyValue(t);
onChange(t);
}}
/>,
nodeClass={nodeClass}
setNodeClass={setNodeClass}
/>
);
}}
>
{!editNode && (
<ExternalLink
strokeWidth={1.5}
className="ml-3 h-6 w-6 hover:text-accent-foreground"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</button>

View file

@ -1,10 +1,11 @@
import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
import { TypeModal } from "../../utils";
import { ExternalLink } from "lucide-react";
import { TabsContext } from "../../contexts/tabsContext";
export default function TextAreaComponent({
value,
@ -14,6 +15,7 @@ export default function TextAreaComponent({
}: TextAreaComponentType) {
const [myValue, setMyValue] = useState(value);
const { openPopUp, closePopUp } = useContext(PopUpContext);
const { setDisableCopyPaste } = useContext(TabsContext);
useEffect(() => {
if (disabled) {
@ -27,37 +29,28 @@ export default function TextAreaComponent({
}, [closePopUp]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div
className={
editNode ? "w-full items-center" : "flex w-full items-center gap-3"
}
>
<span
onClick={() => {
openPopUp(
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>,
);
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<div className="flex w-full items-center">
<input
value={myValue}
onFocus={() => {
setDisableCopyPaste(true);
}}
onBlur={() => {
setDisableCopyPaste(false);
}}
className={
editNode
? "input-edit-node " + " input-dialog "
: " input_dialog " +
"px-3 py-2" +
(disabled ? " input-disable " : "")
? " input-edit-node "
: " input-primary " + (disabled ? " input-disable" : "")
}
>
{myValue !== "" ? myValue : "Type something..."}
</span>
placeholder={"Type something..."}
onChange={(e) => {
setMyValue(e.target.value);
onChange(e.target.value);
}}
/>
<button
onClick={() => {
openPopUp(
@ -70,14 +63,17 @@ export default function TextAreaComponent({
setMyValue(t);
onChange(t);
}}
/>,
/>
);
}}
>
{!editNode && (
<ExternalLink
strokeWidth={1.5}
className="ml-3 h-6 w-6 hover:text-accent-foreground"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</button>

View file

@ -1,7 +1,7 @@
import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { useEffect } from "react";
import { ToggleComponentType } from "../../types/components";
import { classNames } from "../../utils";
export default function ToggleComponent({
enabled,

View file

@ -1,4 +1,3 @@
import { useEffect } from "react";
import { ToggleComponentType } from "../../types/components";
import { Switch } from "../ui/switch";
@ -8,11 +7,6 @@ export default function ToggleShadComponent({
disabled,
size,
}: ToggleComponentType) {
useEffect(() => {
if (disabled) {
setEnabled(false);
}
}, [disabled, setEnabled]);
let scaleX, scaleY;
switch (size) {
case "small":
@ -32,11 +26,12 @@ export default function ToggleShadComponent({
scaleY = 1;
}
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
<Switch
style={{
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}}
disabled={disabled}
className=""
checked={enabled}
onCheckedChange={(x: boolean) => {

View file

@ -1,8 +1,8 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "../../utils";
const Accordion = AccordionPrimitive.Root;
@ -27,8 +27,8 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
@ -47,7 +47,7 @@ const AccordionContent = React.forwardRef<
ref={ref}
className={cn(
"overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
className
)}
{...props}
>

View file

@ -1,34 +1,43 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
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",
"inline-flex items-center border rounded-full px-2.5 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",
gray: "bg-border hover:bg-border/80 text-secondary-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",
},
size: {
sm: "h-4 text-xs",
md: "h-5 text-sm",
lg: "h-6 text-base",
},
},
defaultVariants: {
variant: "default",
},
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
function Badge({ className, variant, size, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
<div
className={cn(badgeVariants({ variant, size }), className)}
{...props}
/>
);
}

View file

@ -1,6 +1,6 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "../../utils";
const buttonVariants = cva(
@ -30,7 +30,7 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
},
}
);
export interface ButtonProps
@ -49,7 +49,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
/>
);
},
}
);
Button.displayName = "Button";

View file

@ -1,8 +1,8 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils";
const Checkbox = React.forwardRef<
@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
className
)}
{...props}
>

View file

@ -1,6 +1,6 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils";
const Dialog = DialogPrimitive.Root;

View file

@ -1,8 +1,8 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
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,
className
)}
{...props}
>
@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
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,
className
)}
{...props}
/>
@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
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,
className
)}
{...props}
/>
@ -84,7 +84,7 @@ const DropdownMenuItem = React.forwardRef<
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,
className
)}
{...props}
/>
@ -99,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
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,
className
)}
checked={checked}
{...props}
@ -123,7 +123,7 @@ const DropdownMenuRadioItem = React.forwardRef<
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,
className
)}
{...props}
>
@ -148,7 +148,7 @@ const DropdownMenuLabel = React.forwardRef<
className={cn(
"px-2 py-1.5 pl-2 text-sm font-semibold",
inset && "pl-8",
className,
className
)}
{...props}
/>

View file

@ -11,13 +11,13 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
className
)}
ref={ref}
{...props}
/>
);
},
}
);
Input.displayName = "Input";

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