Merge branch 'python_custom_node_component' of github.com:logspace-ai/langflow into python_custom_node_component

This commit is contained in:
Lucas Oliveira 2023-07-28 14:49:11 -03:00
commit 1ea05b3584
92 changed files with 4582 additions and 1520 deletions

View file

@ -1,7 +1,7 @@
from importlib import metadata
from langflow.cache import cache_manager
from langflow.processing.process import load_flow_from_json
from langflow.utils.types import Prompt
from langflow.interface.custom.custom_component import CustomComponent
try:
__version__ = metadata.version(__package__)
@ -10,4 +10,4 @@ except metadata.PackageNotFoundError:
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = ["load_flow_from_json", "cache_manager", "Prompt"]
__all__ = ["load_flow_from_json", "cache_manager", "CustomComponent"]

View file

@ -2,8 +2,9 @@ import ast
import inspect
import traceback
from typing import Dict, Any, Type, Union
from typing import Dict, Any, List, Type, Union
from fastapi import HTTPException
from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails
class CodeSyntaxError(HTTPException):
@ -54,13 +55,13 @@ class CodeParser:
return tree
def parse_node(self, node: ast.AST) -> None:
def parse_node(self, node: Union[ast.stmt, ast.AST]) -> None:
"""
Parses an AST node and updates the data
dictionary with the relevant information.
"""
if handler := self.handlers.get(type(node)):
handler(node)
if handler := self.handlers.get(type(node)): # type: ignore
handler(node) # type: ignore
def parse_imports(self, node: Union[ast.Import, ast.ImportFrom]) -> None:
"""
@ -92,27 +93,73 @@ class CodeParser:
"""
Extracts details from a single function or method node.
"""
func = {
"name": node.name,
"doc": ast.get_docstring(node),
"args": [],
"body": [],
"return_type": ast.unparse(node.returns) if node.returns else None,
}
func = CallableCodeDetails(
name=node.name,
doc=ast.get_docstring(node),
args=[],
body=[],
return_type=ast.unparse(node.returns) if node.returns else None,
)
# Handle positional arguments with default values
defaults = [None] * (len(node.args.args) - len(node.args.defaults)) + [
ast.unparse(default) if default else None for default in node.args.defaults
func.args = self.parse_function_args(node)
func.body = self.parse_function_body(node)
return func.dict()
def parse_function_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the arguments of a function or method node.
"""
args = []
args += self.parse_positional_args(node)
args += self.parse_varargs(node)
args += self.parse_keyword_args(node)
args += self.parse_kwargs(node)
return args
def parse_positional_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the positional arguments of a function or method node.
"""
num_args = len(node.args.args)
num_defaults = len(node.args.defaults)
num_missing_defaults = num_args - num_defaults
missing_defaults = [None] * num_missing_defaults
default_values = [
ast.unparse(default).strip("'") if default else None
for default in node.args.defaults
]
# Now check all default values to see if there
# are any "None" values in the middle
default_values = [
None if value == "None" else value for value in default_values
]
for arg, default in zip(node.args.args, defaults):
func["args"].append(self.parse_arg(arg, default))
defaults = missing_defaults + default_values
args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.args, defaults)
]
return args
def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the *args argument of a function or method node.
"""
args = []
# Handle *args
if node.args.vararg:
func["args"].append(self.parse_arg(node.args.vararg, None))
args.append(self.parse_arg(node.args.vararg, None))
# Handle keyword-only arguments with default values
return args
def parse_keyword_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the keyword-only arguments of a function or method node.
"""
kw_defaults = [None] * (
len(node.args.kwonlyargs) - len(node.args.kw_defaults)
) + [
@ -120,16 +167,28 @@ class CodeParser:
for default in node.args.kw_defaults
]
for arg, default in zip(node.args.kwonlyargs, kw_defaults):
func["args"].append(self.parse_arg(arg, default))
args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.kwonlyargs, kw_defaults)
]
return args
def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
"""
Parses the **kwargs argument of a function or method node.
"""
args = []
# Handle **kwargs
if node.args.kwarg:
func["args"].append(self.parse_arg(node.args.kwarg, None))
args.append(self.parse_arg(node.args.kwarg, None))
for line in node.body:
func["body"].append(ast.unparse(line))
return func
return args
def parse_function_body(self, node: ast.FunctionDef) -> List[str]:
"""
Parses the body of a function or method node.
"""
return [ast.unparse(line) for line in node.body]
def parse_assign(self, stmt):
"""
@ -164,29 +223,31 @@ class CodeParser:
"""
Extracts "classes" from the code, including inheritance and init methods.
"""
class_dict = {
"name": node.name,
"doc": ast.get_docstring(node),
"bases": [ast.unparse(base) for base in node.bases],
"attributes": [],
"methods": [],
}
class_details = ClassCodeDetails(
name=node.name,
doc=ast.get_docstring(node),
bases=[ast.unparse(base) for base in node.bases],
attributes=[],
methods=[],
init=None,
)
for stmt in node.body:
if isinstance(stmt, ast.Assign):
if attr := self.parse_assign(stmt):
class_dict["attributes"].append(attr)
class_details.attributes.append(attr)
elif isinstance(stmt, ast.AnnAssign):
if attr := self.parse_ann_assign(stmt):
class_dict["attributes"].append(attr)
class_details.attributes.append(attr)
elif isinstance(stmt, ast.FunctionDef):
method, is_init = self.parse_function_def(stmt)
if is_init:
class_dict["init"] = method
class_details.init = method
else:
class_dict["methods"].append(method)
class_details.methods.append(method)
self.data["classes"].append(class_dict)
self.data["classes"].append(class_details.dict())
def parse_global_vars(self, node: ast.Assign) -> None:
"""

View file

@ -1,4 +1,5 @@
import ast
from typing import Optional
from pydantic import BaseModel
from fastapi import HTTPException
@ -20,7 +21,7 @@ class Component(BaseModel):
"The name of the entrypoint function must be provided."
)
code: str
code: Optional[str]
function_entrypoint_name = "build"
field_config: dict = {}

View file

@ -20,13 +20,22 @@ LANGCHAIN_BASE_TYPES = {
"VectorStore": VectorStore,
"Embeddings": Embeddings,
"BaseRetriever": BaseRetriever,
}
# Langchain base types plus Python base types
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
**LANGCHAIN_BASE_TYPES,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
}
DEFAULT_CUSTOM_COMPONENT_CODE = """
from langflow import Prompt
from langflow.interface.custom.custom_component import CustomComponent
from langflow import CustomComponent
from langchain.llms.base import BaseLLM
from langchain.chains import LLMChain
@ -38,11 +47,12 @@ import requests
class YourComponent(CustomComponent):
display_name: str = "Your Component"
description: str = "Your description"
field_config = { "url": { "multiline": True, "required": True } }
def build(self, url: str, llm: BaseLLM, template: Prompt) -> Document:
def build_config(self):
return { "url": { "multiline": True, "required": True } }
def build(self, url: str, llm: BaseLLM, prompt: PromptTemplate) -> Document:
response = requests.get(url)
prompt = PromptTemplate.from_template(template)
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(response.text[:300])
return Document(page_content=str(result))

View file

@ -1,11 +1,10 @@
from typing import Callable, Optional
from typing import Any, Callable, List, Optional
from fastapi import HTTPException
from langflow.interface.custom.constants import LANGCHAIN_BASE_TYPES
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.custom.component import Component
from langflow.utils import validate
from uuid import UUID
from langflow.database.base import session_getter
from langflow.database.models.flow import Flow
from pydantic import Extra
@ -17,14 +16,14 @@ class CustomComponent(Component, extra=Extra.allow):
code_class_base_inheritance = "CustomComponent"
function_entrypoint_name = "build"
function: Optional[Callable] = None
return_type_valid_list = list(LANGCHAIN_BASE_TYPES.keys())
return_type_valid_list = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
repr_value: Optional[str] = ""
def __init__(self, **data):
super().__init__(**data)
def custom_repr(self):
return self.repr_value
return str(self.repr_value)
def build_config(self):
return self.field_config
@ -44,13 +43,15 @@ class CustomComponent(Component, extra=Extra.allow):
return True
def is_check_valid(self) -> bool:
return self._class_template_validation(self.code)
return self._class_template_validation(self.code) if self.code else False
def get_code_tree(self, code: str):
return super().get_code_tree(code)
@property
def get_function_entrypoint_args(self) -> str:
if not self.code:
return ""
tree = self.get_code_tree(self.code)
component_classes = [
@ -78,6 +79,8 @@ class CustomComponent(Component, extra=Extra.allow):
@property
def get_function_entrypoint_return_type(self) -> str:
if not self.code:
return ""
tree = self.get_code_tree(self.code)
component_classes = [
@ -138,16 +141,19 @@ class CustomComponent(Component, extra=Extra.allow):
def get_function(self):
return validate.create_function(self.code, self.function_entrypoint_name)
def load_flow(self, flow_id: UUID = None):
def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any:
from langflow.processing.process import build_sorted_vertices_with_caching
from langflow.processing.process import process_tweaks
with session_getter() as session:
data_graph = flow.data if (flow := session.get(Flow, flow_id)) else None
if not data_graph:
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
if not graph_data:
raise ValueError(f"Flow {flow_id} not found")
return build_sorted_vertices_with_caching(data_graph)
if tweaks:
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
return build_sorted_vertices_with_caching(graph_data)
def list_flows(self):
def list_flows(self) -> List[Flow]:
with session_getter() as session:
flows = session.query(Flow).all()
return flows

View file

@ -0,0 +1,29 @@
from pydantic import BaseModel, Field
from typing import Optional
class ClassCodeDetails(BaseModel):
"""
A dataclass for storing details about a class.
"""
name: str
doc: Optional[str]
bases: list
attributes: list
methods: list
init: Optional[dict] = Field(default_factory=dict)
class CallableCodeDetails(BaseModel):
"""
A dataclass for storing details about a callable.
"""
name: str
doc: Optional[str]
args: list
body: list
return_type: Optional[str]

View file

@ -1,17 +1,15 @@
import contextlib
import json
from typing import Any, Callable, Dict, List, Sequence, Type
from typing import Any, Callable, Dict, Sequence, Type
from langchain.agents import ZeroShotAgent
from langchain.agents import agent as agent_module
from langchain.agents.agent import AgentExecutor
from langchain.agents.agent_toolkits.base import BaseToolkit
from langchain.agents.tools import BaseTool
from langflow.interface.initialize.llm import initialize_vertexai
from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type
from langflow.interface.initialize.vector_store import vecstore_initializer
from langchain.schema import Document, BaseOutputParser
from pydantic import ValidationError
from langflow.interface.importing.utils import (
@ -212,68 +210,8 @@ def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params:
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)
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)
params, prompt = handle_node_type(node_type, class_object, params)
format_kwargs = handle_format_kwargs(prompt, params)
return prompt, format_kwargs

View file

@ -0,0 +1,103 @@
import contextlib
import json
from typing import Any, Dict, List
from langchain.agents import ZeroShotAgent
from langchain.schema import Document, BaseOutputParser
def handle_node_type(node_type, class_object, params: Dict):
if node_type == "ZeroShotPrompt":
params = check_tools_in_params(params)
prompt = ZeroShotAgent.create_prompt(**params)
elif "MessagePromptTemplate" in node_type:
prompt = instantiate_from_template(class_object, params)
elif node_type == "ChatPromptTemplate":
prompt = class_object.from_messages(**params)
else:
prompt = class_object(**params)
return params, prompt
def check_tools_in_params(params: Dict):
if "tools" not in params:
params["tools"] = []
return params
def instantiate_from_template(class_object, params: Dict):
from_template_params = {
"template": params.pop("prompt", params.pop("template", ""))
}
if not from_template_params.get("template"):
raise ValueError("Prompt template is required")
return class_object.from_template(**from_template_params)
def handle_format_kwargs(prompt, params: Dict):
format_kwargs: Dict[str, Any] = {}
for input_variable in prompt.input_variables:
if input_variable in params:
format_kwargs = handle_variable(params, input_variable, format_kwargs)
return format_kwargs
def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict):
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 is_instance_of_list_or_document(variable):
format_kwargs = format_document(variable, input_variable, format_kwargs)
if needs_handle_keys(variable):
format_kwargs = add_handle_keys(input_variable, format_kwargs)
return format_kwargs
def is_instance_of_list_or_document(variable):
return (
isinstance(variable, List)
and all(isinstance(item, Document) for item in variable)
or isinstance(variable, Document)
)
def format_document(variable, input_variable: str, format_kwargs: Dict):
variable = variable if isinstance(variable, List) else [variable]
content = format_content(variable)
format_kwargs[input_variable] = content
return format_kwargs
def format_content(variable):
if len(variable) > 1:
return "\n".join([item.page_content for item in variable if item.page_content])
content = variable[0].page_content
return try_to_load_json(content)
def try_to_load_json(content):
with contextlib.suppress(json.JSONDecodeError):
content = json.loads(content)
if isinstance(content, list):
content = ",".join([str(item) for item in content])
return content
def needs_handle_keys(variable):
return is_instance_of_list_or_document(variable) or (
isinstance(variable, BaseOutputParser)
and hasattr(variable, "get_format_instructions")
)
def add_handle_keys(input_variable: str, format_kwargs: Dict):
if "handle_keys" not in format_kwargs:
format_kwargs["handle_keys"] = []
format_kwargs["handle_keys"].append(input_variable)
return format_kwargs

View file

@ -55,7 +55,7 @@ TOOL_INPUTS = {
show=True,
value="",
suffixes=[".json", ".yaml", ".yml"],
fileTypes=["json", "yaml", "yml"],
file_types=["json", "yaml", "yml"],
),
}

View file

@ -48,29 +48,3 @@ class PythonFunctionTool(Function, Tool):
class PythonFunction(Function):
code: str
class CustomComponent_old(BaseModel):
code: str
function: Optional[Callable] = None
imports: Optional[str] = None
# Eval code and store the class
def __init__(self, **data):
super().__init__(**data)
# Validate the Class code
@validator("code")
def validate_func(cls, v):
try:
validate.eval_function(v)
except Exception as e:
raise e
return v
def get_function(self):
"""Get the function"""
function_name = validate.extract_function_name(self.code)
return validate.create_function(self.code, function_name)

View file

@ -1,6 +1,6 @@
from langflow.interface.agents.base import agent_creator
from langflow.interface.chains.base import chain_creator
from langflow.interface.custom.constants import LANGCHAIN_BASE_TYPES
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
from langflow.interface.document_loaders.base import documentloader_creator
from langflow.interface.embeddings.base import embedding_creator
from langflow.interface.importing.utils import get_function_custom
@ -98,6 +98,13 @@ def add_new_custom_field(
display_name = field_config.pop("display_name", field_name)
field_type = field_config.pop("field_type", field_type)
field_type = process_type(field_type)
field_value = field_config.pop("value", field_value)
field_advanced = field_config.pop("advanced", False)
# If options is a list, then it's a dropdown
# If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list)
field_config["is_list"] = is_list or field_config.get("is_list", False)
if "name" in field_config:
warnings.warn(
@ -114,7 +121,7 @@ def add_new_custom_field(
value=field_value,
show=True,
required=required,
advanced=False,
advanced=field_advanced,
placeholder=placeholder,
display_name=display_name,
**field_config,
@ -126,8 +133,9 @@ def add_new_custom_field(
# TODO: Move to correct place
def add_code_field(template, raw_code):
def add_code_field(template, raw_code, field_config):
# Field with the Python code to allow update
code_field = {
"code": {
"dynamic": True,
@ -138,7 +146,7 @@ def add_code_field(template, raw_code):
"value": raw_code,
"password": False,
"name": "code",
"advanced": False,
"advanced": field_config.pop("advanced", False),
"type": "code",
"list": False,
}
@ -183,22 +191,30 @@ def update_display_name_and_description(frontend_node, template_config):
frontend_node["description"] = template_config["description"]
def build_field_config(custom_component):
def build_field_config(custom_component: CustomComponent):
"""Build the field configuration for a custom component"""
try:
custom_class = get_function_custom(custom_component.code)
return custom_class().build_config()
except Exception as exc:
logger.error(f"Error while building field config: {exc}")
logger.error(f"Error while getting custom function: {str(exc)}")
return {}
try:
return custom_class().build_config()
except Exception as exc:
logger.error(f"Error while building field config: {str(exc)}")
return {}
def add_extra_fields(frontend_node, field_config, function_args):
"""Add extra fields to the frontend node"""
if function_args is None:
if function_args is None or function_args == "":
return
# sort function_args which is a list of dicts
function_args.sort(key=lambda x: x["name"])
for extra_field in function_args:
if "name" not in extra_field or extra_field["name"] == "self":
continue
@ -232,19 +248,19 @@ def get_field_properties(extra_field):
def add_base_classes(frontend_node, return_type):
"""Add base classes to the frontend node"""
if return_type not in LANGCHAIN_BASE_TYPES or return_type is None:
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
raise HTTPException(
status_code=400,
detail={
"error": (
"Invalid return type should be one of: "
f"{list(LANGCHAIN_BASE_TYPES.keys())}"
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
),
"traceback": traceback.format_exc(),
},
)
return_type_instance = LANGCHAIN_BASE_TYPES.get(return_type)
return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type)
base_classes = get_base_classes(return_type_instance)
for base_class in base_classes:
@ -268,7 +284,9 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
frontend_node, field_config, custom_component.get_function_entrypoint_args
)
frontend_node = add_code_field(frontend_node, custom_component.code)
frontend_node = add_code_field(
frontend_node, custom_component.code, field_config.get("code", {})
)
add_base_classes(
frontend_node, custom_component.get_function_entrypoint_return_type
@ -287,8 +305,8 @@ def load_files_from_path(path: str):
def build_and_validate_all_files(reader, file_list):
"""Build and validate all files"""
data = reader.build_component_menu_list(file_list)
valid_components = reader.filter_loaded_components(data=data, with_errors=False)
valid_components = reader.filter_loaded_components(data=data, with_errors=False)
invalid_components = reader.filter_loaded_components(data=data, with_errors=True)
return valid_components, invalid_components
@ -341,12 +359,15 @@ def build_invalid_menu(invalid_components):
.get(type(CustomComponent()).__name__)
)
component_template["error"] = component.get("error", None)
component_template.get("template").get("code")["value"] = component_code
invalid_menu[menu_name][component_name] = component_template
except Exception as exc:
logger.error(f"Error while creating custom component: {exc}")
logger.error(
f"Error while creating custom component [{component_name}]: {str(exc)}"
)
return invalid_menu

View file

@ -145,7 +145,7 @@ class CSVAgentNode(FrontendNode):
name="path",
value="",
suffixes=[".csv"],
fileTypes=["csv"],
file_types=["csv"],
),
TemplateField(
field_type="BaseLanguageModel",

View file

@ -53,6 +53,7 @@ class FrontendNode(BaseModel):
output_types: List[str] = []
field_formatters: FieldFormatters = Field(default_factory=FieldFormatters)
beta: bool = False
error: Optional[str] = None
# field formatters is an instance attribute but it is not used in the class
# so we need to create a method to get it
@ -85,6 +86,7 @@ class FrontendNode(BaseModel):
"output_types": self.output_types,
"documentation": self.documentation,
"beta": self.beta,
"error": self.error,
},
}

View file

@ -8,6 +8,7 @@ FORCE_SHOW_FIELDS = [
"headers",
"max_value_length",
"max_tokens",
"google_cse_id",
]
DEFAULT_PROMPT = """

View file

@ -14,7 +14,7 @@ def build_file_field(
name=name,
value="",
suffixes=suffixes,
fileTypes=fileTypes,
file_types=fileTypes,
)

View file

@ -19,7 +19,7 @@ class LLMFrontendNode(FrontendNode):
name="credentials",
value="",
suffixes=[".json"],
fileTypes=["json"],
file_types=["json"],
)
)

View file

@ -9,7 +9,7 @@ from docstring_parser import parse # type: ignore
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants
from langflow.utils.logger import logger
from multiprocess import cpu_count
from multiprocess import cpu_count # type: ignore
def build_template_from_function(
@ -301,13 +301,15 @@ def get_type(value: Any) -> Union[str, type]:
return _type if isinstance(_type, str) else _type.__name__
def remove_optional_wrapper(_type: str) -> str:
def remove_optional_wrapper(_type: Union[str, type]) -> str:
"""
Removes the 'Optional' wrapper from the type string.
Returns:
The type string with the 'Optional' wrapper removed.
"""
if isinstance(_type, type):
_type = str(_type)
if "Optional" in _type:
_type = _type.replace("Optional[", "")[:-1]

View file

@ -20,6 +20,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -29,6 +30,7 @@
"@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2",
"ace-builds": "^1.16.0",
"add": "^2.0.6",
@ -143,6 +145,8 @@
},
"node_modules/@babel/compat-data": {
"version": "7.22.9",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
"integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -1460,6 +1464,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.6.tgz",
"integrity": "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.1.2",
"license": "MIT",
@ -2610,6 +2651,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
"dependencies": {
"axios": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"dev": true,
@ -9533,8 +9583,9 @@
"license": "MIT"
},
"node_modules/word-wrap": {
"version": "1.2.3",
"license": "MIT",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"engines": {
"node": ">=0.10.0"
}

View file

@ -15,6 +15,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -24,6 +25,7 @@
"@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2",
"ace-builds": "^1.16.0",
"add": "^2.0.6",

View file

@ -132,7 +132,7 @@ export default function GenericNode({
</span>
) : (
<div className="max-h-96 overflow-auto">
{validationStatus.params
{typeof validationStatus.params === "string"
? validationStatus.params
.split("\n")
.map((line, index) => <div key={index}>{line}</div>)

View file

@ -1,66 +1,72 @@
import { useContext, useRef } from "react";
import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "../../components/ui/popover";
import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext";
import { AlertDropdownType } from "../../types/alerts";
import { useOnClickOutside } from "../hooks/useOnClickOutside";
import SingleAlert from "./components/singleAlertComponent";
export default function AlertDropdown({}: AlertDropdownType) {
const { closePopUp } = useContext(PopUpContext);
const componentRef = useRef<HTMLDivElement>(null);
// Use the custom hook
useOnClickOutside(componentRef, () => {
closePopUp();
});
export default function AlertDropdown({ children }: AlertDropdownType) {
const {
notificationList,
clearNotificationList,
removeFromNotificationList,
setNotificationCenter,
} = useContext(alertContext);
const [open, setOpen] = useState(false);
return (
<div
ref={componentRef}
className="z-10 flex h-[500px] w-[400px] flex-col overflow-hidden rounded-md bg-muted px-2 py-3 pb-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
<Popover
open={open}
onOpenChange={(k) => {
setOpen(k);
if (k) setNotificationCenter(false);
}}
>
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">
<button
className="text-foreground hover:text-status-red"
onClick={() => {
closePopUp();
setTimeout(clearNotificationList, 100);
}}
>
<IconComponent name="Trash2" className="h-[1.1rem] w-[1.1rem]" />
</button>
<button
className="text-foreground hover:text-status-red"
onClick={closePopUp}
>
<IconComponent name="X" className="h-5 w-5" />
</button>
</div>
</div>
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
key={alertItem.id}
dropItem={alertItem}
removeAlert={removeFromNotificationList}
/>
))
) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
No new notifications
<PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent className="flex h-[500px] w-[500px] flex-col">
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">
<button
className="text-foreground hover:text-status-red"
onClick={() => {
setOpen(false);
setTimeout(clearNotificationList, 100);
}}
>
<IconComponent name="Trash2" className="h-[1.1rem] w-[1.1rem]" />
</button>
<button
className="text-foreground hover:text-status-red"
onClick={() => {
setOpen(false);
}}
>
<IconComponent name="X" className="h-5 w-5" />
</button>
</div>
)}
</div>
</div>
</div>
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
{notificationList.length !== 0 ? (
notificationList.map((alertItem, index) => (
<SingleAlert
key={alertItem.id}
dropItem={alertItem}
removeAlert={removeFromNotificationList}
/>
))
) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
No new notifications
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}

View file

@ -12,9 +12,8 @@ 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);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {

View file

@ -45,7 +45,7 @@ export default function CodeAreaComponent({
editNode
? "input-edit-node input-dialog"
: (disabled ? " input-disable input-ring " : "") +
" input-primary text-muted-foreground "
" primary-input text-muted-foreground "
}
>
{myValue !== "" ? myValue : "Type something..."}

View file

@ -145,7 +145,7 @@ export default function CodeTabsComponent({
</TabsTrigger>
))}
</TabsList>
{Number(activeTab) < 3 && (
{Number(activeTab) < 4 && (
<div className="float-right mx-1 flex gap-2">
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
@ -174,15 +174,23 @@ export default function CodeTabsComponent({
className="api-modal-tabs-content"
key={index} // Remember to add a unique key prop
>
{index < 3 ? (
<SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll"
language={tab.mode}
style={oneDark}
>
{tab.code}
</SyntaxHighlighter>
) : index === 3 ? (
{index < 4 ? (
<>
{tab.description && (
<div
className="mb-2 w-full text-left text-sm"
dangerouslySetInnerHTML={{ __html: tab.description }}
></div>
)}
<SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll"
language={tab.mode}
style={oneDark}
>
{tab.code}
</SyntaxHighlighter>
</>
) : index === 4 ? (
<>
<div className="api-modal-according-display">
<div

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { Input } from "../ui/input";
export default function FloatComponent({
value,
@ -19,8 +20,8 @@ export default function FloatComponent({
}, [disabled, onChange]);
return (
<div className={"w-full " + (disabled ? "float-component-pointer" : "")}>
<input
<div className="w-full">
<Input
type="number"
step={step}
min={min}
@ -34,12 +35,8 @@ export default function FloatComponent({
}}
max={max}
value={value ?? ""}
className={
"nopan nodrag noundo nocopy " +
(editNode
? "input-edit-node"
: "input-primary" + (disabled ? " input-disable " : ""))
}
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={
editNode ? "Number 0 to 1" : "Type a number from zero to one"
}

View file

@ -1,13 +1,11 @@
import { useContext, useEffect, useState } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { Link, useLocation, useParams } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/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 { getRepoStars } from "../../controllers/API";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
@ -15,14 +13,9 @@ import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar";
export default function Header() {
const { flows, addFlow, tabId } = useContext(TabsContext);
const { openPopUp } = useContext(PopUpContext);
const { templates } = useContext(typesContext);
const { id } = useParams();
const AlertWidth = 384;
const { flows, tabId } = useContext(TabsContext);
const { dark, setDark } = useContext(darkContext);
const { notificationCenter, setNotificationCenter, setErrorData } =
useContext(alertContext);
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const [stars, setStars] = useState(null);
@ -111,33 +104,18 @@ export default function Header() {
<IconComponent name="MoonIcon" className="side-bar-button-size" />
)}
</button>
<button
className="extra-side-bar-save-disable relative"
onClick={(event: React.MouseEvent<HTMLElement>) => {
setNotificationCenter(false);
const { top, left } = (
event.target as Element
).getBoundingClientRect();
openPopUp(
<>
<div
className="absolute z-10"
style={{ top: top + 40, left: left - AlertWidth }}
>
<AlertDropdown />
</div>
<div className="header-notifications-box"></div>
</>
);
}}
>
{notificationCenter && <div className="header-notifications"></div>}
<IconComponent
name="Bell"
className="side-bar-button-size"
aria-hidden="true"
/>
</button>
<AlertDropdown>
<div className="extra-side-bar-save-disable relative">
{notificationCenter && (
<div className="header-notifications"></div>
)}
<IconComponent
name="Bell"
className="side-bar-button-size"
aria-hidden="true"
/>
</div>
</AlertDropdown>
</div>
</div>
</div>

View file

@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { InputComponentType } from "../../types/components";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
export default function InputComponent({
value,
@ -19,16 +20,15 @@ export default function InputComponent({
}, [disabled, onChange]);
return (
<div className={disabled ? "input-component-div" : "relative"}>
<input
<div className="relative w-full">
<Input
value={value}
disabled={disabled}
className={classNames(
disabled ? " input-disable " : "",
password && !pwdVisible && value !== "" ? " text-clip password " : "",
editNode ? " input-edit-node " : " input-primary ",
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
"nopan nodrag noundo nocopy"
password && !editNode ? "pr-10" : ""
)}
placeholder={password && editNode ? "Key" : "Type something..."}
onChange={(e) => {

View file

@ -102,8 +102,8 @@ export default function InputFileComponent({
editNode
? "input-edit-node input-dialog text-muted-foreground"
: disabled
? "input-disable input-dialog input-primary"
: "input-dialog input-primary text-muted-foreground"
? "input-disable input-dialog primary-input"
: "input-dialog primary-input text-muted-foreground"
}
>
{myValue !== "" ? myValue : "No file"}

View file

@ -2,7 +2,9 @@ import { useEffect } from "react";
import { InputListComponentType } from "../../types/components";
import _ from "lodash";
import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
import { Input } from "../ui/input";
export default function InputListComponent({
value,
@ -18,23 +20,19 @@ export default function InputListComponent({
return (
<div
className={
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
}
)}
>
{value.map((i, idx) => {
return (
<div key={idx} className="flex w-full gap-3">
<input
<Input
disabled={disabled}
type="text"
value={i}
className={
"nopan nodrag noundo nocopy " +
(editNode
? "input-edit-node "
: "input-primary " + (disabled ? "input-disable" : ""))
}
className={editNode ? "input-edit-node" : ""}
placeholder="Type something..."
onChange={(e) => {
let newInputList = _.cloneDeep(value);

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { Input } from "../ui/input";
export default function IntComponent({
value,
@ -17,13 +18,8 @@ export default function IntComponent({
}, [disabled, onChange]);
return (
<div
className={
"w-full " +
(disabled ? "pointer-events-none w-full cursor-not-allowed" : "")
}
>
<input
<div className="w-full">
<Input
onKeyDown={(event) => {
if (
event.key !== "Backspace" &&
@ -51,12 +47,8 @@ export default function IntComponent({
}
}}
value={value ?? ""}
className={
"nopan nodrag noundo nocopy " +
(editNode
? " input-edit-node "
: " input-primary " + (disabled ? " input-disable" : ""))
}
className={editNode ? "input-edit-node" : ""}
disabled={disabled}
placeholder={editNode ? "Integer number" : "Type an integer number"}
onChange={(e) => {
onChange(e.target.value);

View file

@ -51,7 +51,7 @@ export default function PromptAreaComponent({
editNode
? "input-edit-node input-dialog"
: (disabled ? " input-disable text-ring " : "") +
" input-primary text-muted-foreground "
" primary-input text-muted-foreground "
}
>
{value !== "" ? value : "Type your prompt here..."}

View file

@ -3,6 +3,7 @@ import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Input } from "../ui/input";
export default function TextAreaComponent({
value,
@ -18,42 +19,36 @@ export default function TextAreaComponent({
}, [disabled]);
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<div className="flex w-full items-center">
<input
<div className="flex w-full items-center">
<Input
value={value}
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={"Type something..."}
onChange={(e) => {
onChange(e.target.value);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
className={
(editNode
? " input-edit-node "
: " input-primary " + (disabled ? " input-disable" : "")) +
" nopan nodrag noundo nocopy w-full"
}
placeholder={"Type something..."}
onChange={(e) => {
onChange(e.target.value);
setValue={(t: string) => {
onChange(t);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
setValue={(t: string) => {
onChange(t);
}}
>
{!editNode && (
<IconComponent
name="ExternalLink"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</GenericModal>
</div>
>
{!editNode && (
<IconComponent
name="ExternalLink"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</GenericModal>
</div>
</div>
);

View file

@ -9,10 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
return (
<input
type={type}
className={cn(
"nopan nodrag noundo nocopy 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={cn("nopan nodrag noundo nocopy primary-input", className)}
ref={ref}
{...props}
/>

View file

@ -0,0 +1,30 @@
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "../../utils/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View file

@ -8,10 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background 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={cn("nopan nodrag noundo nocopy textarea-primary", className)}
ref={ref}
{...props}
/>

View file

@ -491,3 +491,14 @@ export const NOUNS: string[] = [
*
*/
export const USER_PROJECTS_HEADER = "My Collection";
/**
* URLs excluded from error retries.
* @constant
*
*/
export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
];

View file

@ -5,7 +5,6 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { DarkProvider } from "./darkContext";
import { LocationProvider } from "./locationContext";
import PopUpProvider from "./popUpContext";
import { TabsProvider } from "./tabsContext";
import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext";
@ -22,9 +21,7 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
<AlertProvider>
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>
<PopUpProvider>{children}</PopUpProvider>
</UndoRedoProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
</SSEProvider>
</AlertProvider>

View file

@ -1,38 +0,0 @@
import React, { createContext, useState } from "react";
// context to set JSX element on the DOM
export const PopUpContext = createContext({
openPopUp: (popUpElement: JSX.Element) => {},
closePopUp: () => {},
setCloseEdit: (value: string) => {},
closeEdit: "",
});
interface PopUpProviderProps {
children: React.ReactNode;
}
const PopUpProvider = ({ children }: PopUpProviderProps) => {
const [popUpElements, setPopUpElements] = useState<JSX.Element[]>([]);
const openPopUp = (element: JSX.Element) => {
setPopUpElements((prevPopUps) => [element, ...prevPopUps]);
};
const closePopUp = () => {
setPopUpElements((prevPopUps) => prevPopUps.slice(1));
};
const [closeEdit, setCloseEdit] = useState("");
return (
<PopUpContext.Provider
value={{ openPopUp, closePopUp, closeEdit, setCloseEdit }}
>
{children}
{popUpElements[0]}
</PopUpContext.Provider>
);
};
export default PopUpProvider;

View file

@ -40,6 +40,8 @@ const TabsContextInitialValue: TabsContextType = {
downloadFlows: () => {},
uploadFlows: () => {},
uploadFlow: () => {},
isBuilt: false,
setIsBuilt: (state: boolean) => {},
hardReset: () => {},
saveFlow: async (flow: FlowType) => {},
lastCopiedSelection: null,
@ -583,10 +585,14 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
}
const [isBuilt, setIsBuilt] = useState(false);
return (
<TabsContext.Provider
value={{
saveFlow,
isBuilt,
setIsBuilt,
lastCopiedSelection,
setLastCopiedSelection,
hardReset,

View file

@ -71,18 +71,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Clear the interval if successful.
clearInterval(intervalId);
} catch (error) {
retryCount++;
// On error, double the delay for the next attempt up to a maximum.
delay = Math.min(30000, delay * 2);
// Log errors but don't do anything else - the function will try again on the next interval.
console.error(error);
// Clear the old interval and start a new one with the new delay.
if (retryCount <= maxRetryCount) {
clearInterval(intervalId);
intervalId = setInterval(getTypes, delay);
} else {
console.error("Max retry attempts reached. Stopping retries.");
}
console.error("An error has occurred while fetching types.");
}
}

View file

@ -0,0 +1,62 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect, useRef } from "react";
import { URL_EXCLUDED_FROM_ERROR_RETRIES } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
baseURL: "",
});
function ApiInterceptor() {
const retryCounts = useRef([]);
const { setErrorData } = useContext(alertContext);
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
return Promise.reject(error);
}
let retryCount = 0;
while (retryCount < 4) {
await sleep(5000); // Sleep for 5 seconds
retryCount++;
try {
const response = await axios.request(error.config);
return response;
} catch (error) {
if (retryCount === 3) {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
"Refresh the page",
"Use a new flow tab",
"Check if the backend is up",
"Endpoint: " + error.config?.url,
],
});
return Promise.reject(error);
}
}
}
}
);
return () => {
// Clean up the interceptor when the component unmounts
api.interceptors.response.eject(interceptor);
};
}, [retryCounts]);
return null;
}
// Function to sleep for a given duration in milliseconds
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export { ApiInterceptor, api };

View file

@ -1,5 +1,6 @@
import axios, { AxiosResponse } from "axios";
import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow";
import { api } from "../../controllers/API/api";
import { APIObjectType, sendAllProps } from "../../types/api/index";
import { FlowStyleType, FlowType } from "../../types/flow";
import {
@ -17,16 +18,14 @@ import {
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
*/
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/api/v1/all`);
return await api.get(`/api/v1/all`);
}
const GITHUB_API_URL = "https://api.github.com";
export async function getRepoStars(owner, repo) {
try {
const response = await axios.get(
`${GITHUB_API_URL}/repos/${owner}/${repo}`
);
const response = await api.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`);
return response.data.stargazers_count;
} catch (error) {
console.error("Error fetching repository data:", error);
@ -41,13 +40,13 @@ export async function getRepoStars(owner, repo) {
* @returns {AxiosResponse<any>} The API response.
*/
export async function sendAll(data: sendAllProps) {
return await axios.post(`/api/v1/predict`, data);
return await api.post(`/api/v1/predict`, data);
}
export async function postValidateCode(
code: string
): Promise<AxiosResponse<errorsTypeAPI>> {
return await axios.post("/api/v1/validate/code", { code });
return await api.post("/api/v1/validate/code", { code });
}
/**
@ -62,7 +61,7 @@ export async function postValidatePrompt(
template: string,
frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> {
return await axios.post("/api/v1/validate/prompt", {
return await api.post("/api/v1/validate/prompt", {
name: name,
template: template,
frontend_node: frontend_node,
@ -77,14 +76,14 @@ export async function postValidatePrompt(
export async function getExamples(): Promise<FlowType[]> {
const url =
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples?ref=main";
const response = await axios.get(url);
const response = await api.get(url);
const jsonFiles = response.data.filter((file: any) => {
return file.name.endsWith(".json");
});
const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await axios.get(file.download_url);
const contentResponse = await api.get(file.download_url);
return contentResponse.data;
});
@ -106,11 +105,12 @@ export async function saveFlowToDatabase(newFlow: {
style?: FlowStyleType;
}): Promise<FlowType> {
try {
const response = await axios.post("/api/v1/flows/", {
const response = await api.post("/api/v1/flows/", {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -131,7 +131,7 @@ export async function updateFlowInDatabase(
updatedFlow: FlowType
): Promise<FlowType> {
try {
const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, {
const response = await api.patch(`/api/v1/flows/${updatedFlow.id}`, {
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
@ -155,7 +155,7 @@ export async function updateFlowInDatabase(
*/
export async function readFlowsFromDatabase() {
try {
const response = await axios.get("/api/v1/flows/");
const response = await api.get("/api/v1/flows/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -168,7 +168,7 @@ export async function readFlowsFromDatabase() {
export async function downloadFlowsFromDatabase() {
try {
const response = await axios.get("/api/v1/flows/download/");
const response = await api.get("/api/v1/flows/download/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -181,7 +181,7 @@ export async function downloadFlowsFromDatabase() {
export async function uploadFlowsToDatabase(flows) {
try {
const response = await axios.post(`/api/v1/flows/upload/`, flows);
const response = await api.post(`/api/v1/flows/upload/`, flows);
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
@ -202,7 +202,7 @@ export async function uploadFlowsToDatabase(flows) {
*/
export async function deleteFlowFromDatabase(flowId: string) {
try {
const response = await axios.delete(`/api/v1/flows/${flowId}`);
const response = await api.delete(`/api/v1/flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -222,7 +222,7 @@ export async function deleteFlowFromDatabase(flowId: string) {
*/
export async function getFlowFromDatabase(flowId: number) {
try {
const response = await axios.get(`/api/v1/flows/${flowId}`);
const response = await api.get(`/api/v1/flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -241,7 +241,7 @@ export async function getFlowFromDatabase(flowId: number) {
*/
export async function getFlowStylesFromDatabase() {
try {
const response = await axios.get("/api/v1/flow_styles/");
const response = await api.get("/api/v1/flow_styles/");
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -261,7 +261,7 @@ export async function getFlowStylesFromDatabase() {
*/
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
try {
const response = await axios.post("/api/v1/flow_styles/", flowStyle, {
const response = await api.post("/api/v1/flow_styles/", flowStyle, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
@ -284,7 +284,7 @@ export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information.
*/
export async function getVersion() {
const respnose = await axios.get("/api/v1/version");
const respnose = await api.get("/api/v1/version");
return respnose.data;
}
@ -294,7 +294,7 @@ export async function getVersion() {
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status.
*/
export async function getHealth() {
return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1
return await api.get("/health"); // Health is the only endpoint that doesn't require /api/v1
}
/**
@ -306,7 +306,7 @@ export async function getHealth() {
export async function getBuildStatus(
flowId: string
): Promise<BuildStatusTypeAPI> {
return await axios.get(`/api/v1/build/${flowId}/status`);
return await api.get(`/api/v1/build/${flowId}/status`);
}
//docs for postbuildinit
@ -319,7 +319,7 @@ export async function getBuildStatus(
export async function postBuildInit(
flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> {
return await axios.post(`/api/v1/build/init/${flow.id}`, flow);
return await api.post(`/api/v1/build/init/${flow.id}`, flow);
}
// fetch(`/upload/${id}`, {
@ -337,12 +337,12 @@ export async function uploadFile(
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
return await axios.post(`/api/v1/upload/${id}`, formData);
return await api.post(`/api/v1/upload/${id}`, formData);
}
export async function postCustomComponent(
code: string,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
return await axios.post(`/api/v1/custom_component`, { code });
return await api.post(`/api/v1/custom_component`, { code });
}

View file

@ -4,7 +4,13 @@ import App from "./App";
import ContextWrapper from "./contexts";
import reportWebVitals from "./reportWebVitals";
import "./index.css";
import { ApiInterceptor } from "./controllers/API/api";
// @ts-ignore
import "./style/index.css";
// @ts-ignore
import "./style/applies.css";
// @ts-ignore
import "./style/classes.css";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
@ -13,6 +19,7 @@ root.render(
<ContextWrapper>
<BrowserRouter>
<App />
<ApiInterceptor />
</BrowserRouter>
</ContextWrapper>
);

View file

@ -21,6 +21,7 @@ import {
getCurlCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
} from "../../utils/utils";
import BaseModal from "../baseModal";
@ -29,9 +30,11 @@ const ApiModal = forwardRef(
{
flow,
children,
disable,
}: {
flow: FlowType;
children: ReactNode;
disable: boolean;
},
ref
) => {
@ -43,6 +46,7 @@ const ApiModal = forwardRef(
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState);
const curl_code = getCurlCode(flow, tweak.current, tabsState);
const pythonCode = getPythonCode(flow, tweak.current, tabsState);
const widgetCode = getWidgetCode(flow, tabsState);
const tweaksCode = buildTweaks(flow);
const [tabs, setTabs] = useState([
{
@ -67,6 +71,15 @@ const ApiModal = forwardRef(
language: "py",
code: pythonCode,
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: widgetCode,
},
]);
function startState() {
@ -111,6 +124,15 @@ const ApiModal = forwardRef(
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: pythonCode,
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: widgetCode,
},
{
name: "Tweaks",
mode: "python",
@ -143,6 +165,15 @@ const ApiModal = forwardRef(
language: "py",
code: pythonCode,
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: widgetCode,
},
]);
}
}, [flow["data"]["nodes"], open]);
@ -210,13 +241,15 @@ const ApiModal = forwardRef(
tweak.current.push(newTweak);
}
const pythonApiCode = getPythonApiCode(flow, tweak.current);
const curl_code = getCurlCode(flow, tweak.current);
const pythonCode = getPythonCode(flow, tweak.current);
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState);
const curl_code = getCurlCode(flow, tweak.current, tabsState);
const pythonCode = getPythonCode(flow, tweak.current, tabsState);
const widgetCode = getWidgetCode(flow, tabsState);
tabs[0].code = curl_code;
tabs[1].code = pythonApiCode;
tabs[2].code = pythonCode;
tabs[3].code = widgetCode;
setTweak(tweak.current);
}
@ -253,7 +286,7 @@ const ApiModal = forwardRef(
}
return (
<BaseModal open={open} setOpen={setOpen}>
<BaseModal open={open} setOpen={setOpen} disable={disable}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">Code</span>

View file

@ -46,11 +46,13 @@ interface BaseModalProps {
];
open?: boolean;
setOpen?: (open: boolean) => void;
disable?: boolean;
size?: "smaller" | "small" | "medium" | "large" | "large-h-full";
}
function BaseModal({
open,
setOpen,
disable = false,
children,
size = "large",
}: BaseModalProps) {
@ -99,7 +101,10 @@ function BaseModal({
//UPDATE COLORS AND STYLE CLASSSES
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full" hidden={triggerChild ? false : true}>
<DialogTrigger
className={"w-full " + (disable ? "button-disable" : "")}
hidden={triggerChild ? false : true}
>
{triggerChild}
</DialogTrigger>
<DialogContent className={minWidth}>

View file

@ -96,6 +96,7 @@ export default function CodeAreaModal({
if (data) {
setNodeClass(data);
setValue(code);
setError({ detail: { error: undefined, traceback: undefined } });
setOpen(false);
}
})

View file

@ -1,5 +1,6 @@
import { useEffect } from "react";
import IconComponent from "../../../components/genericIconComponent";
import { Textarea } from "../../../components/ui/textarea";
import { classNames } from "../../../utils/utils";
export default function ChatInput({
@ -25,7 +26,7 @@ export default function ChatInput({
return (
<div className="relative">
<textarea
<Textarea
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage();
@ -68,10 +69,10 @@ export default function ChatInput({
className={classNames(
"form-modal-send-button",
noInput
? "bg-indigo-600 text-background"
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-emerald-600 text-background"
: "bg-chat-send text-background"
)}
disabled={lockChat}
onClick={() => sendMessage()}

View file

@ -395,7 +395,7 @@ export default function FormModal({
<span className="pr-2">Chat</span>
<IconComponent
name="prompts"
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</DialogTitle>

View file

@ -1,6 +1,7 @@
import { useContext, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext";
import { TabsContext } from "../../../../contexts/tabsContext";
@ -18,7 +19,7 @@ import DisclosureComponent from "../DisclosureComponent";
export default function ExtraSidebar() {
const { data } = useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow } =
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
useContext(TabsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
@ -60,56 +61,72 @@ export default function ExtraSidebar() {
return (
<div className="side-bar-arrangement">
<div className="side-bar-buttons-arrangement">
<ShadTooltip content="Import" side="top">
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
</button>
</ShadTooltip>
<ShadTooltip content="Export" side="top">
<div className="side-bar-button">
<ShadTooltip content="Import" side="top">
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
</button>
</ShadTooltip>
</div>
<div className="side-bar-button">
<ExportModal>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</div>
</ExportModal>
</ShadTooltip>
<ShadTooltip content="Code" side="top">
{flow && flow.data && (
<ApiModal flow={flow}>
<ShadTooltip content="Export" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent name="Code2" className="side-bar-button-size" />
<IconComponent
name="FileDown"
className="side-bar-button-size"
/>
</div>
</ApiModal>
)}
</ShadTooltip>
</ExportModal>
</div>
<ShadTooltip content={"Code"} side="top">
<div className="side-bar-button">
{flow && flow.data && (
<ApiModal flow={flow} disable={!isBuilt}>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="Code2"
className={
"side-bar-button-size" +
(isBuilt ? " " : " extra-side-bar-save-disable")
}
/>
</div>
</ApiModal>
)}
</div>
</ShadTooltip>
<ShadTooltip content="Save" side="top">
<button
className="extra-side-bar-buttons"
onClick={(event) => {
saveFlow(flow);
setSuccessData({ title: "Changes saved successfully" });
}}
disabled={!isPending}
>
<IconComponent
name="Save"
<div className="side-bar-button">
<ShadTooltip content="Save" side="top">
<button
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
"extra-side-bar-buttons " + (isPending ? "" : "button-disable")
}
/>
</button>
</ShadTooltip>
onClick={(event) => {
saveFlow(flow);
setSuccessData({ title: "Changes saved successfully" });
}}
>
<IconComponent
name="Save"
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
}
/>
</button>
</ShadTooltip>
</div>
</div>
<Separator />
<div className="side-bar-search-div-placement">
<input
<Input
type="text"
name="search"
id="search"

View file

@ -2,127 +2,6 @@
@tailwind components;
@tailwind utilities;
/* TODO: Confirm that all colors here are found in tailwind config */
@layer base {
:root {
--background: 0 0% 100%; /* hsl(0 0% 100%) */
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--card: 0 0% 100%; /* hsl(0 0% 100%) */
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063;
--error-background: #fef2f2;
--error-foreground: #991b1b;
--success-background: #f0fdf4;
--success-foreground: #14532d;
--info-background: #f0f4fd;
--info-foreground: #141653;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--chat-bot-icon: #afe6ef;
--chat-user-icon: #aface9;
--beta-background: rgb(219 234 254);
--beta-foreground: rgb(37 99 235);
/* Colors that are shared in dark and light mode */
--blur-shared: #151923de;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #b4c3da;
--status-red: #ef4444;
--status-yellow: #eab308;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
}
.dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--card: 224 25% 15.5%; /* hsl(224 71% 4%) */
--card-foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--border: 216 24% 17%; /* hsl(216 34% 17%) */
--input: 216 24% 17%; /* hsl(216 34% 17%) */
--primary: 210 20% 80%; /* hsl(210 20% 80%) */
--primary-foreground: 222.2 27.4% 1.2%; /* hsl(222 47% 1%) */
--secondary: 222.2 37.4% 7.2%; /* hsl(222 47% 11%) */
--secondary-foreground: 210 40% 80%; /* hsl(210 40% 80%) */
--accent: 216 24% 20%; /* hsl(216 34% 17%) */
--accent-foreground: 210 30% 98%; /* hsl(210 40% 98%) */
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 216 24% 30%; /* hsl(216 24% 30%) */
--radius: 0.5rem;
--round-btn-shadow: #00000063;
--success-background: #022c22;
--success-foreground: #ecfdf5;
--error-foreground: #fef2f2;
--error-background: #450a0a;
--info-foreground: #eff6ff;
--info-background: #172554;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--beta-background: rgb(37 99 235);
--beta-foreground: rgb(219 234 254);
/* Colors that are shared in dark and light mode */
--blur-shared: #151923d2;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #2d3b54;
--status-red: #ef4444;
--status-yellow: #eab308;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
--chat-bot-icon: #235d70;
--chat-user-icon: #4f3d6e;
}
}
@layer base {
* {
@apply border-border;
@ -134,39 +13,6 @@
}
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
pre {
font-family: inherit;
}
/* The style below sets the cursor property of the element with the class .react-flow__pane to the default cursor.
The cursor: default; property value restores the browser's default cursor style for the targeted element. By applying this style, the element will no longer have a custom cursor appearance such as "grab" or any other custom cursor defined elsewhere in the application. Instead, it will revert to the default cursor style determined by the browser, typically an arrow-shaped cursor. */
.react-flow__pane {
cursor: default;
}
.AccordionContent {
overflow: hidden;
}
.AccordionContent[data-state="open"] {
animation: slideDown 300ms ease-out;
}
.AccordionContent[data-state="closed"] {
animation: slideUp 300ms ease-out;
}
@keyframes slideDown {
from {
height: 0;
@ -185,6 +31,36 @@ The cursor: default; property value restores the browser's default cursor style
}
}
@keyframes gradient-motion-start {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 130, 184);
}
80% {
stop-color: rgb(255, 165, 100);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
@keyframes gradient-motion-end {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 165, 100);
}
80% {
stop-color: rgb(255, 130, 184);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
@layer components {
.round-buttons-position {
@apply fixed right-4;
@ -231,6 +107,12 @@ The cursor: default; property value restores the browser's default cursor style
.side-bar-buttons-arrangement {
@apply mb-2 mt-2 flex w-full items-center justify-between gap-2 px-2;
}
.side-bar-button {
@apply flex w-full;
}
.button-disable {
@apply pointer-events-none;
}
.extra-side-bar-buttons {
@apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out;
}
@ -240,18 +122,20 @@ The cursor: default; property value restores the browser's default cursor style
.button-div-style {
@apply flex gap-2;
}
.input-primary:focus {
@apply focus:border-ring focus:placeholder-transparent focus:ring-ring;
}
.input-primary {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground sm:text-sm;
.primary-input {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
}
.input-edit-node{
@apply input-primary border-border pt-0.5 pb-0.5 text-left w-full
/* The same as primary-input but no-truncate */
.textarea-primary {
@apply form-input block w-full rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
}
.input-edit-node {
@apply primary-input w-full pb-0.5 pt-0.5 text-left;
}
.input-search {
@apply input-primary mx-2 pr-7;
@apply primary-input mx-2 pr-7;
}
.input-disable {
@apply border-transparent bg-border placeholder:text-ring;
@ -375,7 +259,7 @@ The cursor: default; property value restores the browser's default cursor style
}
.generic-node-status-position {
@apply relative top-[1.5px] h-5 w-5;
@apply relative top-[3px] h-5 w-5;
}
.generic-node-status-animation {
@ -536,7 +420,7 @@ The cursor: default; property value restores the browser's default cursor style
@apply input-edit-node relative pr-8;
}
.dropdown-component-false-outline {
@apply input-primary py-2 pl-3 pr-10 text-left;
@apply primary-input py-2 pl-3 pr-10 text-left;
}
.dropdown-component-display {
@apply block w-full truncate bg-background;
@ -551,7 +435,7 @@ The cursor: default; property value restores the browser's default cursor style
@apply z-10 mt-1 max-h-60 overflow-auto rounded-md bg-background py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm;
}
.dropdown-component-true-options {
@apply dropdown-component-options lg:w-[32%]
@apply dropdown-component-options lg:w-[32%];
}
.dropdown-component-false-options {
@apply dropdown-component-options w-full;
@ -597,7 +481,7 @@ The cursor: default; property value restores the browser's default cursor style
}
.header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-b bg-muted;
@apply flex-max-width h-12 items-center justify-between border-border bg-muted;
}
.header-start-display {
@apply flex w-96 items-center justify-start gap-2;
@ -902,6 +786,9 @@ The cursor: default; property value restores the browser's default cursor style
.node-modal-button-box {
@apply flex-max-width flex-row-reverse bg-input px-4 pb-3;
}
.link-color {
@apply font-semibold text-foreground;
}
.node-modal-button {
@apply inline-flex w-full justify-center rounded-md border border-transparent bg-status-red px-4 py-2 text-base font-medium text-background shadow-sm hover:bg-ring sm:ml-3 sm:w-auto sm:text-sm;
}
@ -1117,54 +1004,19 @@ The cursor: default; property value restores the browser's default cursor style
.ace-editor-save-btn {
@apply flex-max-width h-fit justify-end;
}
.export-modal-save-api {
@apply font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
}
.beta-badge-wrapper {
@apply absolute right-0 top-0 h-16 w-16 overflow-hidden rounded-tr-lg;
}
.beta-badge-content {
@apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground
}
.export-modal-save-api {
@apply font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
@apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground;
}
.chat-message-highlight {
@apply rounded-md bg-indigo-100 px-0.5 dark:bg-indigo-900;
}
}
@keyframes gradient-motion-start {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 130, 184);
}
80% {
stop-color: rgb(255, 165, 100);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
@keyframes gradient-motion-end {
0% {
stop-color: rgb(156, 138, 236);
}
50% {
stop-color: rgb(255, 165, 100);
}
80% {
stop-color: rgb(255, 130, 184);
}
100% {
stop-color: rgb(156, 138, 236);
}
}
.gradient-end {
animation: gradient-motion-end 3s infinite forwards;
}
.gradient-start {
animation: gradient-motion-start 4s infinite forwards;
}

View file

@ -0,0 +1,38 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
pre {
font-family: inherit;
}
.react-flow__pane {
cursor: default;
}
.AccordionContent {
overflow: hidden;
}
.AccordionContent[data-state="open"] {
animation: slideDown 300ms ease-out;
}
.AccordionContent[data-state="closed"] {
animation: slideUp 300ms ease-out;
}
.gradient-end {
animation: gradient-motion-end 3s infinite forwards;
}
.gradient-start {
animation: gradient-motion-start 4s infinite forwards;
}

View file

@ -0,0 +1,128 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* TODO: Confirm that all colors here are found in tailwind config */
@layer base {
:root {
--background: 0 0% 100%; /* hsl(0 0% 100%) */
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--card: 0 0% 100%; /* hsl(0 0% 100%) */
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063;
--error-background: #fef2f2;
--error-foreground: #991b1b;
--success-background: #f0fdf4;
--success-foreground: #14532d;
--info-background: #f0f4fd;
--info-foreground: #141653;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--low-indigo: #e0e7ff;
--beta-background: rgb(219 234 254);
--beta-foreground: rgb(37 99 235);
--chat-bot-icon: #afe6ef;
--chat-user-icon: #aface9;
/* Colors that are shared in dark and light mode */
--blur-shared: #151923de;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #b4c3da;
--status-red: #ef4444;
--status-yellow: #eab308;
--chat-send: #059669;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
}
.dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--card: 224 25% 15.5%; /* hsl(224 71% 4%) */
--card-foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--border: 216 24% 17%; /* hsl(216 34% 17%) */
--input: 216 24% 17%; /* hsl(216 34% 17%) */
--primary: 210 20% 80%; /* hsl(210 20% 80%) */
--primary-foreground: 222.2 27.4% 1.2%; /* hsl(222 47% 1%) */
--secondary: 222.2 37.4% 7.2%; /* hsl(222 47% 11%) */
--secondary-foreground: 210 40% 80%; /* hsl(210 40% 80%) */
--accent: 216 24% 20%; /* hsl(216 34% 17%) */
--accent-foreground: 210 30% 98%; /* hsl(210 40% 98%) */
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 216 24% 30%; /* hsl(216 24% 30%) */
--radius: 0.5rem;
--round-btn-shadow: #00000063;
--success-background: #022c22;
--success-foreground: #ecfdf5;
--error-foreground: #fef2f2;
--error-background: #450a0a;
--info-foreground: #eff6ff;
--info-background: #172554;
--high-indigo: #4338ca;
--medium-indigo: #6366f1;
--low-indigo: #e0e7ff;
/* Colors that are shared in dark and light mode */
--blur-shared: #151923d2;
--build-trigger: #dc735b;
--chat-trigger: #5c8be1;
--chat-trigger-disabled: #2d3b54;
--status-red: #ef4444;
--status-yellow: #eab308;
--chat-send: #059669;
--status-green: #4ade80;
--status-blue: #2563eb;
--connection: #555;
--beta-background: rgb(37 99 235);
--beta-foreground: rgb(219 234 254);
--chat-bot-icon: #235d70;
--chat-user-icon: #4f3d6e;
}
}

View file

@ -19,7 +19,9 @@ export type SingleAlertComponentType = {
dropItem: AlertItemType;
removeAlert: (index: string) => void;
};
export type AlertDropdownType = {};
export type AlertDropdownType = {
children: JSX.Element;
};
export type AlertItemType = {
type: "notice" | "error" | "success";
title: string;

View file

@ -18,6 +18,8 @@ export type TabsContextType = {
) => void;
downloadFlows: () => void;
uploadFlows: () => void;
isBuilt: boolean;
setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow?: boolean, file?: File) => void;
hardReset: () => void;
getNodeId: (nodeType: string) => string;

View file

@ -467,7 +467,7 @@ export function getCurlCode(
/**
* Function to get the python code for the API
* @param {string} flowName - The name of the flow
* @param {string} flow - The current flow
* @returns {string} - The python code
*/
export function getPythonCode(
@ -489,3 +489,33 @@ flow = load_flow_from_json("${flowName}.json", tweaks=TWEAKS)
inputs = ${inputs}
flow(inputs)`;
}
/**
* Function to get the widget code for the API
* @param {string} flow - The current flow.
* @returns {string} - The widget code
*/
export function getWidgetCode(flow: FlowType, tabsState?: TabsState): string {
const flowId = flow.id;
const flowName = flow.name;
const inputs = buildInputs(tabsState, flow.id);
return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@main/dist/build/static/js/bundle.min.js"></script>
<!-- chat_inputs: Stringified JSON with all the input keys and its values. The value of the key that is defined
as chat_input_field will be overwritten by the chat message.
chat_input_field: Input key that you want the chat to send the user message with. -->
<langflow-chat
window_title="${flowName}"
flow_id="${flowId}"
${
tabsState[flow.id] && tabsState[flow.id].formKeysData
? `chat_inputs='${inputs}'
chat_input_field="${
Object.keys(tabsState[flow.id].formKeysData.input_keys)[0]
}"
`
: ""
}host_url="http://localhost:7860"
></langflow-chat>`;
}

View file

@ -29,6 +29,8 @@ module.exports = {
},
extend: {
colors: {
"low-indigo": "var(--low-indigo)",
"chat-send": "var(--chat-send)",
connection: "var(--connection)",
"almost-dark-gray": "var(--almost-dark-gray)",
"almost-light-blue": "var(--almost-light-blue)",