Adding Support for Key-Value Pair Objects in Input Flows (#845)
This commit is contained in:
commit
311b0a57e9
37 changed files with 619 additions and 207 deletions
|
|
@ -33,6 +33,7 @@ The CustomComponent class serves as the foundation for creating custom component
|
||||||
| Supported Types |
|
| Supported Types |
|
||||||
| --------------------------------------------------------- |
|
| --------------------------------------------------------- |
|
||||||
| _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ |
|
| _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ |
|
||||||
|
| _`langflow.field_typing.NestedDict`_ |
|
||||||
| _`langchain.chains.base.Chain`_ |
|
| _`langchain.chains.base.Chain`_ |
|
||||||
| _`langchain.PromptTemplate`_ |
|
| _`langchain.PromptTemplate`_ |
|
||||||
| _`langchain.llms.base.BaseLLM`_ |
|
| _`langchain.llms.base.BaseLLM`_ |
|
||||||
|
|
@ -44,6 +45,8 @@ The CustomComponent class serves as the foundation for creating custom component
|
||||||
| _`langchain.embeddings.base.Embeddings`_ |
|
| _`langchain.embeddings.base.Embeddings`_ |
|
||||||
| _`langchain.schema.BaseRetriever`_ |
|
| _`langchain.schema.BaseRetriever`_ |
|
||||||
|
|
||||||
|
The difference between _`dict`_ and _`langflow.field_typing.NestedDict`_ is that one adds a simple key-value pair field, while the other opens a more robust dictionary editor.
|
||||||
|
|
||||||
<Admonition type="info">
|
<Admonition type="info">
|
||||||
Unlike Langchain types, base Python types do not add a
|
Unlike Langchain types, base Python types do not add a
|
||||||
[handle](../guidelines/components) to the field by default. To add handles,
|
[handle](../guidelines/components) to the field by default. To add handles,
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,14 @@ class PromptRunner(CustomComponent):
|
||||||
"info": "Make sure the prompt has all variables filled.",
|
"info": "Make sure the prompt has all variables filled.",
|
||||||
},
|
},
|
||||||
"code": {"show": False},
|
"code": {"show": False},
|
||||||
"inputs": {"field_type": "code"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}
|
||||||
llm: BaseLLM,
|
|
||||||
prompt: PromptTemplate,
|
|
||||||
) -> Document:
|
) -> Document:
|
||||||
chain = prompt | llm
|
chain = prompt | llm
|
||||||
# The input is an empty dict because the prompt is already filled
|
# The input is an empty dict because the prompt is already filled
|
||||||
result = chain.invoke({})
|
result = chain.invoke(input=inputs)
|
||||||
if hasattr(result, "content"):
|
if hasattr(result, "content"):
|
||||||
result = result.content
|
result = result.content
|
||||||
self.repr_value = result
|
self.repr_value = result
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class MetalRetrieverComponent(CustomComponent):
|
||||||
"api_key": {"display_name": "API Key", "password": True},
|
"api_key": {"display_name": "API Key", "password": True},
|
||||||
"client_id": {"display_name": "Client ID", "password": True},
|
"client_id": {"display_name": "Client ID", "password": True},
|
||||||
"index_id": {"display_name": "Index ID"},
|
"index_id": {"display_name": "Index ID"},
|
||||||
"params": {"display_name": "Parameters", "field_type": "code"},
|
"params": {"display_name": "Parameters"},
|
||||||
"code": {"show": False},
|
"code": {"show": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ class GetRequest(CustomComponent):
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"display_name": "Headers",
|
"display_name": "Headers",
|
||||||
"field_type": "code",
|
|
||||||
"info": "The headers to send with the request.",
|
"info": "The headers to send with the request.",
|
||||||
},
|
},
|
||||||
"code": {"show": False},
|
"code": {"show": False},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ class PostRequest(CustomComponent):
|
||||||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||||
"headers": {
|
"headers": {
|
||||||
"display_name": "Headers",
|
"display_name": "Headers",
|
||||||
"field_type": "code",
|
|
||||||
"info": "The headers to send with the request.",
|
"info": "The headers to send with the request.",
|
||||||
},
|
},
|
||||||
"code": {"show": False},
|
"code": {"show": False},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class UpdateRequest(CustomComponent):
|
||||||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||||
"headers": {
|
"headers": {
|
||||||
"display_name": "Headers",
|
"display_name": "Headers",
|
||||||
"field_type": "code",
|
"field_type": "NestedDict",
|
||||||
"info": "The headers to send with the request.",
|
"info": "The headers to send with the request.",
|
||||||
},
|
},
|
||||||
"code": {"show": False},
|
"code": {"show": False},
|
||||||
|
|
|
||||||
3
src/backend/langflow/field_typing/__init__.py
Normal file
3
src/backend/langflow/field_typing/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .base import NestedDict
|
||||||
|
|
||||||
|
__all__ = ["NestedDict"]
|
||||||
4
src/backend/langflow/field_typing/base.py
Normal file
4
src/backend/langflow/field_typing/base.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from typing import Union, Dict
|
||||||
|
|
||||||
|
# Type alias for more complex dicts
|
||||||
|
NestedDict = Dict[str, Union[str, Dict]]
|
||||||
|
|
@ -122,6 +122,19 @@ class Vertex:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(f"Error parsing code: {exc}")
|
logger.debug(f"Error parsing code: {exc}")
|
||||||
params[key] = value.get("value")
|
params[key] = value.get("value")
|
||||||
|
elif value.get("type") in ["dict", "NestedDict"]:
|
||||||
|
# When dict comes from the frontend it comes as a
|
||||||
|
# list of dicts, so we need to convert it to a dict
|
||||||
|
# before passing it to the build method
|
||||||
|
_value = value.get("value")
|
||||||
|
if isinstance(_value, list):
|
||||||
|
params[key] = {
|
||||||
|
k: v
|
||||||
|
for item in value.get("value", [])
|
||||||
|
for k, v in item.items()
|
||||||
|
}
|
||||||
|
elif isinstance(_value, dict):
|
||||||
|
params[key] = _value
|
||||||
else:
|
else:
|
||||||
params[key] = value.get("value")
|
params[key] = value.get("value")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ def get_memory_key(langchain_object):
|
||||||
"history": "chat_history",
|
"history": "chat_history",
|
||||||
}
|
}
|
||||||
# Check if memory_key attribute exists
|
# Check if memory_key attribute exists
|
||||||
if hasattr(langchain_object.memory, 'memory_key'):
|
if hasattr(langchain_object.memory, "memory_key"):
|
||||||
memory_key = langchain_object.memory.memory_key
|
memory_key = langchain_object.memory.memory_key
|
||||||
return mem_key_dict.get(memory_key)
|
return mem_key_dict.get(memory_key)
|
||||||
else:
|
else:
|
||||||
|
|
@ -90,5 +90,4 @@ def update_memory_keys(langchain_object, possible_new_mem_key):
|
||||||
try:
|
try:
|
||||||
setattr(langchain_object.memory, attr, key)
|
setattr(langchain_object.memory, attr, key)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.debug(
|
logger.debug(f"{langchain_object.memory} has no attribute {attr} ({exc})")
|
||||||
f"{langchain_object.memory} has no attribute {attr} ({exc})")
|
|
||||||
|
|
|
||||||
|
|
@ -140,13 +140,16 @@ class FrontendNode(BaseModel):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_dict_type(field: TemplateField, _type: str) -> str:
|
def handle_dict_type(field: TemplateField, _type: str) -> str:
|
||||||
"""Handles 'dict' type by replacing it with 'code' or 'file' based on the field name."""
|
"""Handles 'dict' type by replacing it with 'code' or 'file' based on the field name."""
|
||||||
if "dict" in _type.lower():
|
if "dict" in _type.lower() and field.name == "dict_":
|
||||||
if field.name == "dict_":
|
field.field_type = "file"
|
||||||
field.field_type = "file"
|
field.suffixes = [".json", ".yaml", ".yml"]
|
||||||
field.suffixes = [".json", ".yaml", ".yml"]
|
field.file_types = ["json", "yaml", "yml"]
|
||||||
field.file_types = ["json", "yaml", "yml"]
|
elif (
|
||||||
else:
|
_type.startswith("Dict")
|
||||||
field.field_type = "code"
|
or _type.startswith("Mapping")
|
||||||
|
or _type.startswith("dict")
|
||||||
|
):
|
||||||
|
field.field_type = "dict"
|
||||||
return _type
|
return _type
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -240,20 +243,6 @@ class FrontendNode(BaseModel):
|
||||||
"description",
|
"description",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def replace_dict_with_code_or_file(
|
|
||||||
field: TemplateField, _type: str, key: str
|
|
||||||
) -> str:
|
|
||||||
"""Replaces 'dict' type with 'code' or 'file'."""
|
|
||||||
if "dict" in _type.lower():
|
|
||||||
if key == "dict_":
|
|
||||||
field.field_type = "file"
|
|
||||||
field.suffixes = [".json", ".yaml", ".yml"]
|
|
||||||
field.file_types = ["json", "yaml", "yml"]
|
|
||||||
else:
|
|
||||||
field.field_type = "code"
|
|
||||||
return field.field_type
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_field_default_value(field: TemplateField, value: dict, key: str) -> None:
|
def set_field_default_value(field: TemplateField, value: dict, key: str) -> None:
|
||||||
"""Sets the field value with the default value if present."""
|
"""Sets the field value with the default value if present."""
|
||||||
|
|
|
||||||
|
|
@ -170,11 +170,11 @@ class DocumentLoaderFrontNode(FrontendNode):
|
||||||
# add a metadata field of type dict
|
# add a metadata field of type dict
|
||||||
self.template.add_field(
|
self.template.add_field(
|
||||||
TemplateField(
|
TemplateField(
|
||||||
field_type="code",
|
field_type="dict",
|
||||||
required=True,
|
required=True,
|
||||||
show=True,
|
show=True,
|
||||||
name="metadata",
|
name="metadata",
|
||||||
value="{}",
|
value={},
|
||||||
display_name="Metadata",
|
display_name="Metadata",
|
||||||
multiline=False,
|
multiline=False,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ class EmbeddingFrontendNode(FrontendNode):
|
||||||
if field.name == "headers":
|
if field.name == "headers":
|
||||||
field.show = False
|
field.show = False
|
||||||
if field.name == "model_kwargs":
|
if field.name == "model_kwargs":
|
||||||
field.field_type = "code"
|
field.field_type = "dict"
|
||||||
field.advanced = True
|
field.advanced = True
|
||||||
field.show = True
|
field.show = True
|
||||||
elif field.name in [
|
elif field.name in [
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,13 @@ class DictCodeFileFormatter(FieldFormatter):
|
||||||
key = field.name
|
key = field.name
|
||||||
value = field.to_dict()
|
value = field.to_dict()
|
||||||
_type = value["type"]
|
_type = value["type"]
|
||||||
if "dict" in _type.lower():
|
if "dict" in _type.lower() and key == "dict_":
|
||||||
if key == "dict_":
|
field.field_type = "file"
|
||||||
field.field_type = "file"
|
field.suffixes = [".json", ".yaml", ".yml"]
|
||||||
field.suffixes = [".json", ".yaml", ".yml"]
|
field.file_types = ["json", "yaml", "yml"]
|
||||||
field.file_types = ["json", "yaml", "yml"]
|
elif (
|
||||||
else:
|
_type.startswith("Dict")
|
||||||
field.field_type = "code"
|
or _type.startswith("Mapping")
|
||||||
|
or _type.startswith("dict")
|
||||||
|
):
|
||||||
|
field.field_type = "dict"
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ class LLMFrontendNode(FrontendNode):
|
||||||
if display_name := display_names_dict.get(field.name):
|
if display_name := display_names_dict.get(field.name):
|
||||||
field.display_name = display_name
|
field.display_name = display_name
|
||||||
if field.name == "model_kwargs":
|
if field.name == "model_kwargs":
|
||||||
field.field_type = "code"
|
field.field_type = "dict"
|
||||||
field.advanced = True
|
field.advanced = True
|
||||||
field.show = True
|
field.show = True
|
||||||
elif field.name in [
|
elif field.name in [
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from langflow.template.template.base import Template
|
||||||
class PromptFrontendNode(FrontendNode):
|
class PromptFrontendNode(FrontendNode):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||||
|
FrontendNode.format_field(field, name)
|
||||||
# if field.field_type == "StringPromptTemplate"
|
# if field.field_type == "StringPromptTemplate"
|
||||||
# change it to str
|
# change it to str
|
||||||
PROMPT_FIELDS = [
|
PROMPT_FIELDS = [
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,4 @@ class UtilitiesFrontendNode(FrontendNode):
|
||||||
field.field_type = "str"
|
field.field_type = "str"
|
||||||
|
|
||||||
if isinstance(field.value, dict):
|
if isinstance(field.value, dict):
|
||||||
field.field_type = "code"
|
|
||||||
field.value = orjson_dumps(field.value)
|
field.value = orjson_dumps(field.value)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class VectorStoreFrontendNode(FrontendNode):
|
||||||
# Add search_kwargs field
|
# Add search_kwargs field
|
||||||
extra_field = TemplateField(
|
extra_field = TemplateField(
|
||||||
name="search_kwargs",
|
name="search_kwargs",
|
||||||
field_type="code",
|
field_type="NestedDict",
|
||||||
required=False,
|
required=False,
|
||||||
placeholder="",
|
placeholder="",
|
||||||
show=True,
|
show=True,
|
||||||
|
|
|
||||||
|
|
@ -48,4 +48,14 @@ def python_function(text: str) -> str:
|
||||||
return text
|
return text
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"]
|
DIRECT_TYPES = [
|
||||||
|
"str",
|
||||||
|
"bool",
|
||||||
|
"dict",
|
||||||
|
"int",
|
||||||
|
"float",
|
||||||
|
"Any",
|
||||||
|
"prompt",
|
||||||
|
"code",
|
||||||
|
"NestedDict",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -276,8 +276,6 @@ def format_dict(
|
||||||
value["password"] = is_password_field(key)
|
value["password"] = is_password_field(key)
|
||||||
value["multiline"] = is_multiline_field(key)
|
value["multiline"] = is_multiline_field(key)
|
||||||
|
|
||||||
replace_dict_type_with_code(value)
|
|
||||||
|
|
||||||
if key == "dict_":
|
if key == "dict_":
|
||||||
set_dict_file_attributes(value)
|
set_dict_file_attributes(value)
|
||||||
|
|
||||||
|
|
@ -407,14 +405,6 @@ def is_multiline_field(key: str) -> bool:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def replace_dict_type_with_code(value: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Replaces the type value with 'code' if the type is a dict.
|
|
||||||
"""
|
|
||||||
if "dict" in value["type"].lower():
|
|
||||||
value["type"] = "code"
|
|
||||||
|
|
||||||
|
|
||||||
def set_dict_file_attributes(value: Dict[str, Any]) -> None:
|
def set_dict_file_attributes(value: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the file attributes for the 'dict_' key.
|
Sets the file attributes for the 'dict_' key.
|
||||||
|
|
|
||||||
9
src/frontend/package-lock.json
generated
9
src/frontend/package-lock.json
generated
|
|
@ -58,6 +58,7 @@
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-tabs": "^6.0.2",
|
"react-tabs": "^6.0.2",
|
||||||
"react-tooltip": "^5.21.1",
|
"react-tooltip": "^5.21.1",
|
||||||
|
"react18-json-view": "^0.2.3",
|
||||||
"reactflow": "^11.8.3",
|
"reactflow": "^11.8.3",
|
||||||
"rehype-mathjax": "^4.0.3",
|
"rehype-mathjax": "^4.0.3",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
|
@ -8872,6 +8873,14 @@
|
||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react18-json-view": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react18-json-view/-/react18-json-view-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-FZlEWS6A3yf1CyuPRtxYrGNZVRvUT1jQvt/kS1C3SDrStwViENsAKhe3NANq37on9cPoNbLD8+KZB+EArNhuWQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reactflow": {
|
"node_modules/reactflow": {
|
||||||
"version": "11.8.3",
|
"version": "11.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.8.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-tabs": "^6.0.2",
|
"react-tabs": "^6.0.2",
|
||||||
"react-tooltip": "^5.21.1",
|
"react-tooltip": "^5.21.1",
|
||||||
|
"react18-json-view": "^0.2.3",
|
||||||
"reactflow": "^11.8.3",
|
"reactflow": "^11.8.3",
|
||||||
"rehype-mathjax": "^4.0.3",
|
"rehype-mathjax": "^4.0.3",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -3,45 +3,78 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
.App-logo {
|
||||||
height: 40vmin;
|
height: 40vmin;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
.App-logo {
|
.App-logo {
|
||||||
animation: App-logo-spin infinite 20s linear;
|
animation: App-logo-spin infinite 20s linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-header {
|
.App-header {
|
||||||
background-color: #282c34;
|
background-color: #282c34;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: calc(10px + 2vmin);
|
font-size: calc(10px + 2vmin);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-link {
|
.App-link {
|
||||||
color: #61dafb;
|
color: #61dafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
@keyframes App-logo-spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: text-security-disc;
|
font-family: text-security-disc;
|
||||||
src: url("assets/text-security-disc.woff") format("woff");
|
src: url("assets/text-security-disc.woff") format("woff");
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-view {
|
||||||
|
height: 370px !important;
|
||||||
|
background-color: #2c2c2c !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jv-indent {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
max-height: 310px !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jv-indent::-webkit-scrollbar {
|
||||||
|
width: 8px !important;
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jv-indent::-webkit-scrollbar-track {
|
||||||
|
background-color: #f1f1f1 !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jv-indent::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #ccc !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jv-indent::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #bbb !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import React, {
|
||||||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||||
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||||
|
import DictComponent from "../../../../components/dictComponent";
|
||||||
import Dropdown from "../../../../components/dropdownComponent";
|
import Dropdown from "../../../../components/dropdownComponent";
|
||||||
import FloatComponent from "../../../../components/floatComponent";
|
import FloatComponent from "../../../../components/floatComponent";
|
||||||
import IconComponent from "../../../../components/genericIconComponent";
|
import IconComponent from "../../../../components/genericIconComponent";
|
||||||
|
|
@ -16,6 +17,7 @@ import InputComponent from "../../../../components/inputComponent";
|
||||||
import InputFileComponent from "../../../../components/inputFileComponent";
|
import InputFileComponent from "../../../../components/inputFileComponent";
|
||||||
import InputListComponent from "../../../../components/inputListComponent";
|
import InputListComponent from "../../../../components/inputListComponent";
|
||||||
import IntComponent from "../../../../components/intComponent";
|
import IntComponent from "../../../../components/intComponent";
|
||||||
|
import KeypairListComponent from "../../../../components/keypairListComponent";
|
||||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||||
|
|
@ -24,7 +26,12 @@ import { TabsContext } from "../../../../contexts/tabsContext";
|
||||||
import { typesContext } from "../../../../contexts/typesContext";
|
import { typesContext } from "../../../../contexts/typesContext";
|
||||||
import { ParameterComponentType } from "../../../../types/components";
|
import { ParameterComponentType } from "../../../../types/components";
|
||||||
import { TabsState } from "../../../../types/tabs";
|
import { TabsState } from "../../../../types/tabs";
|
||||||
import { isValidConnection } from "../../../../utils/reactflowUtils";
|
import {
|
||||||
|
convertObjToArray,
|
||||||
|
convertValuesToNumbers,
|
||||||
|
hasDuplicateKeys,
|
||||||
|
isValidConnection,
|
||||||
|
} from "../../../../utils/reactflowUtils";
|
||||||
import {
|
import {
|
||||||
nodeColors,
|
nodeColors,
|
||||||
nodeIconsLucide,
|
nodeIconsLucide,
|
||||||
|
|
@ -74,13 +81,18 @@ export default function ParameterComponent({
|
||||||
|
|
||||||
const { data: myData } = useContext(typesContext);
|
const { data: myData } = useContext(typesContext);
|
||||||
|
|
||||||
const handleOnNewValue = (newValue: string | string[] | boolean): void => {
|
const handleOnNewValue = (
|
||||||
|
newValue: string | string[] | boolean | Object[]
|
||||||
|
): void => {
|
||||||
let newData = cloneDeep(data);
|
let newData = cloneDeep(data);
|
||||||
newData.node!.template[name].value = newValue;
|
newData.node!.template[name].value = newValue;
|
||||||
setData(newData);
|
setData(newData);
|
||||||
// Set state to pending
|
// Set state to pending
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
setTabsState((prev: TabsState) => {
|
setTabsState((prev: TabsState) => {
|
||||||
|
if (!prev[tabId]) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[tabId]: {
|
[tabId]: {
|
||||||
|
|
@ -93,6 +105,8 @@ export default function ParameterComponent({
|
||||||
renderTooltips();
|
renderTooltips();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (name === "openai_api_base") console.log(info);
|
if (name === "openai_api_base") console.log(info);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -214,6 +228,8 @@ export default function ParameterComponent({
|
||||||
type === "code" ||
|
type === "code" ||
|
||||||
type === "prompt" ||
|
type === "prompt" ||
|
||||||
type === "file" ||
|
type === "file" ||
|
||||||
|
type === "dict" ||
|
||||||
|
type === "NestedDict" ||
|
||||||
type === "int") &&
|
type === "int") &&
|
||||||
!optionalHandle ? (
|
!optionalHandle ? (
|
||||||
<></>
|
<></>
|
||||||
|
|
@ -350,6 +366,42 @@ export default function ParameterComponent({
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : left === true && type === "NestedDict" ? (
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<DictComponent
|
||||||
|
disabled={disabled}
|
||||||
|
editNode={false}
|
||||||
|
value={
|
||||||
|
data.node!.template[name].value ?? {
|
||||||
|
yourkey: "value",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
data.node!.template[name].value = newValue;
|
||||||
|
handleOnNewValue(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : left === true && type === "dict" ? (
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<KeypairListComponent
|
||||||
|
disabled={disabled}
|
||||||
|
editNode={false}
|
||||||
|
value={
|
||||||
|
data.node!.template[name].value?.length === 0 ||
|
||||||
|
!data.node!.template[name].value
|
||||||
|
? [{ "": "" }]
|
||||||
|
: convertObjToArray(data.node!.template[name].value)
|
||||||
|
}
|
||||||
|
duplicateKey={errorDuplicateKey}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||||
|
data.node!.template[name].value = valueToNumbers;
|
||||||
|
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||||
|
handleOnNewValue(valueToNumbers);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
53
src/frontend/src/components/dictComponent/index.tsx
Normal file
53
src/frontend/src/components/dictComponent/index.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { DictComponentType } from "../../types/components";
|
||||||
|
|
||||||
|
import DictAreaModal from "../../modals/dictAreaModal";
|
||||||
|
import { classNames } from "../../utils/utils";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
|
export default function DictComponent({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
editNode = false,
|
||||||
|
}: DictComponentType): JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled) {
|
||||||
|
onChange([""]);
|
||||||
|
}
|
||||||
|
}, [disabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) onChange(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
value.length > 1 && editNode ? "my-1" : "",
|
||||||
|
"flex flex-col gap-3"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<div className="flex w-full gap-3">
|
||||||
|
<DictAreaModal
|
||||||
|
value={value}
|
||||||
|
onChange={(obj) => {
|
||||||
|
onChange(obj);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className={
|
||||||
|
editNode
|
||||||
|
? "input-edit-node input-disable pointer-events-none cursor-pointer"
|
||||||
|
: "input-disable pointer-events-none cursor-pointer"
|
||||||
|
}
|
||||||
|
placeholder="Click to edit your dictionary..."
|
||||||
|
/>
|
||||||
|
</DictAreaModal>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
src/frontend/src/components/keypairListComponent/index.tsx
Normal file
112
src/frontend/src/components/keypairListComponent/index.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { KeyPairListComponentType } from "../../types/components";
|
||||||
|
|
||||||
|
import _ from "lodash";
|
||||||
|
import { classNames } from "../../utils/utils";
|
||||||
|
import IconComponent from "../genericIconComponent";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
|
export default function KeypairListComponent({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
editNode = false,
|
||||||
|
duplicateKey,
|
||||||
|
}: KeyPairListComponentType): JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled) {
|
||||||
|
onChange([""]);
|
||||||
|
}
|
||||||
|
}, [disabled]);
|
||||||
|
|
||||||
|
const handleChangeKey = (event, idx) => {
|
||||||
|
const newInputList = _.cloneDeep(value);
|
||||||
|
const oldKey = Object.keys(newInputList[idx])[0];
|
||||||
|
const updatedObj = { [event.target.value]: newInputList[idx][oldKey] };
|
||||||
|
newInputList[idx] = updatedObj;
|
||||||
|
onChange(newInputList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeValue = (newValue, idx) => {
|
||||||
|
const newInputList = _.cloneDeep(value);
|
||||||
|
const key = Object.keys(newInputList[idx])[0];
|
||||||
|
newInputList[idx][key] = newValue;
|
||||||
|
onChange(newInputList);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) onChange(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
value?.length > 1 && editNode ? "my-1" : "",
|
||||||
|
"flex flex-col gap-3"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value?.map((obj, index) => {
|
||||||
|
return Object.keys(obj).map((key, idx) => {
|
||||||
|
return (
|
||||||
|
<div key={idx} className="flex w-full gap-3">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={key.trim()}
|
||||||
|
className={classNames(
|
||||||
|
editNode ? "input-edit-node" : "",
|
||||||
|
duplicateKey ? "input-invalid" : ""
|
||||||
|
)}
|
||||||
|
placeholder="Type key..."
|
||||||
|
onChange={(event) => handleChangeKey(event, index)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.ctrlKey && e.key === "Backspace") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={obj[key]}
|
||||||
|
className={editNode ? "input-edit-node" : ""}
|
||||||
|
placeholder="Type a value..."
|
||||||
|
onChange={(event) =>
|
||||||
|
handleChangeValue(event.target.value, index)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{index === value.length - 1 ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
let newInputList = _.cloneDeep(value);
|
||||||
|
newInputList.push({ "": "" });
|
||||||
|
onChange(newInputList);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name="Plus"
|
||||||
|
className={"h-4 w-4 hover:text-accent-foreground"}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
let newInputList = _.cloneDeep(value);
|
||||||
|
newInputList.splice(index, 1);
|
||||||
|
onChange(newInputList);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name="X"
|
||||||
|
className="h-4 w-4 hover:text-status-red"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ const DialogPortal = ({
|
||||||
...props
|
...props
|
||||||
}: DialogPrimitive.DialogPortalProps) => (
|
}: DialogPrimitive.DialogPortalProps) => (
|
||||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
<div className="nopan nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</DialogPrimitive.Portal>
|
</DialogPrimitive.Portal>
|
||||||
|
|
@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"nopan nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,9 @@ export const EDIT_DIALOG_SUBTITLE =
|
||||||
export const CODE_PROMPT_DIALOG_SUBTITLE =
|
export const CODE_PROMPT_DIALOG_SUBTITLE =
|
||||||
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
|
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
|
||||||
|
|
||||||
|
export const CODE_DICT_DIALOG_SUBTITLE =
|
||||||
|
"Edit your dictionary. This dialog allows you to create your own customized dictionary. You can add as many key-value pairs as you want. While in edit mode, you can enter ({}) or ([]), and this will result in adding a new object or array.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base text for subtitle of Prompt Dialog
|
* The base text for subtitle of Prompt Dialog
|
||||||
* @constant
|
* @constant
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
|
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
|
||||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||||
|
import DictComponent from "../../components/dictComponent";
|
||||||
import Dropdown from "../../components/dropdownComponent";
|
import Dropdown from "../../components/dropdownComponent";
|
||||||
import FloatComponent from "../../components/floatComponent";
|
import FloatComponent from "../../components/floatComponent";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
|
@ -8,6 +9,7 @@ import InputComponent from "../../components/inputComponent";
|
||||||
import InputFileComponent from "../../components/inputFileComponent";
|
import InputFileComponent from "../../components/inputFileComponent";
|
||||||
import InputListComponent from "../../components/inputListComponent";
|
import InputListComponent from "../../components/inputListComponent";
|
||||||
import IntComponent from "../../components/intComponent";
|
import IntComponent from "../../components/intComponent";
|
||||||
|
import KeypairListComponent from "../../components/keypairListComponent";
|
||||||
import PromptAreaComponent from "../../components/promptComponent";
|
import PromptAreaComponent from "../../components/promptComponent";
|
||||||
import TextAreaComponent from "../../components/textAreaComponent";
|
import TextAreaComponent from "../../components/textAreaComponent";
|
||||||
import ToggleShadComponent from "../../components/toggleShadComponent";
|
import ToggleShadComponent from "../../components/toggleShadComponent";
|
||||||
|
|
@ -26,6 +28,10 @@ import { TabsContext } from "../../contexts/tabsContext";
|
||||||
import { typesContext } from "../../contexts/typesContext";
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
import { NodeDataType } from "../../types/flow";
|
import { NodeDataType } from "../../types/flow";
|
||||||
import { TabsState } from "../../types/tabs";
|
import { TabsState } from "../../types/tabs";
|
||||||
|
import {
|
||||||
|
convertObjToArray,
|
||||||
|
hasDuplicateKeys,
|
||||||
|
} from "../../utils/reactflowUtils";
|
||||||
import { classNames } from "../../utils/utils";
|
import { classNames } from "../../utils/utils";
|
||||||
import BaseModal from "../baseModal";
|
import BaseModal from "../baseModal";
|
||||||
|
|
||||||
|
|
@ -78,6 +84,25 @@ const EditNodeModal = forwardRef(
|
||||||
setMyData(data); // reset data to what it is on node when opening modal
|
setMyData(data); // reset data to what it is on node when opening modal
|
||||||
}, [modalOpen]);
|
}, [modalOpen]);
|
||||||
|
|
||||||
|
const [obj, setObj] = useState({
|
||||||
|
arr: ["test", 123456, false, null],
|
||||||
|
boolean: false,
|
||||||
|
longString:
|
||||||
|
"long string long string long string long string long string long string",
|
||||||
|
number: 123456,
|
||||||
|
try: {
|
||||||
|
k1: 123,
|
||||||
|
k2: "123",
|
||||||
|
k3: false,
|
||||||
|
},
|
||||||
|
string: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||||
|
const [dictArr, setDictArr] = useState([
|
||||||
|
{ yourKey: "yourValue" },
|
||||||
|
] as Object[]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}>
|
<BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}>
|
||||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||||
|
|
@ -166,6 +191,52 @@ const EditNodeModal = forwardRef(
|
||||||
handleOnNewValue(value, templateParam);
|
handleOnNewValue(value, templateParam);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : myData.node?.template[templateParam]
|
||||||
|
.type === "NestedDict" ? (
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<DictComponent
|
||||||
|
disabled={disabled}
|
||||||
|
editNode={true}
|
||||||
|
value={obj}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
setObj(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : myData.node?.template[templateParam]
|
||||||
|
.type === "dict" ? (
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<KeypairListComponent
|
||||||
|
disabled={disabled}
|
||||||
|
editNode={false}
|
||||||
|
value={
|
||||||
|
myData.node.template[templateParam]
|
||||||
|
.value?.length === 0 ||
|
||||||
|
!myData.node.template[templateParam]
|
||||||
|
.value
|
||||||
|
? dictArr
|
||||||
|
: convertObjToArray(
|
||||||
|
myData.node.template[
|
||||||
|
templateParam
|
||||||
|
].value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
duplicateKey={errorDuplicateKey}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
setErrorDuplicateKey(
|
||||||
|
hasDuplicateKeys(newValue)
|
||||||
|
);
|
||||||
|
if (hasDuplicateKeys(newValue)) {
|
||||||
|
setDictArr(newValue);
|
||||||
|
} else {
|
||||||
|
setDictArr(newValue);
|
||||||
|
myData.node!.template[
|
||||||
|
templateParam
|
||||||
|
].value = newValue;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : myData.node.template[templateParam]
|
) : myData.node.template[templateParam]
|
||||||
.multiline ? (
|
.multiline ? (
|
||||||
<TextAreaComponent
|
<TextAreaComponent
|
||||||
|
|
|
||||||
67
src/frontend/src/modals/dictAreaModal/index.tsx
Normal file
67
src/frontend/src/modals/dictAreaModal/index.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import "ace-builds/src-noconflict/ace";
|
||||||
|
import "ace-builds/src-noconflict/ext-language_tools";
|
||||||
|
import "ace-builds/src-noconflict/mode-python";
|
||||||
|
import "ace-builds/src-noconflict/theme-github";
|
||||||
|
import "ace-builds/src-noconflict/theme-twilight";
|
||||||
|
// import "ace-builds/webpack-resolver";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import JsonView from "react18-json-view";
|
||||||
|
import "react18-json-view/src/dark.css";
|
||||||
|
import "react18-json-view/src/style.css";
|
||||||
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { CODE_DICT_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||||
|
import BaseModal from "../baseModal";
|
||||||
|
|
||||||
|
export default function DictAreaModal({
|
||||||
|
children,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
}): JSX.Element {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [dictObj, setDictObj] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) setDictObj(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
||||||
|
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||||
|
<BaseModal.Header description={CODE_DICT_DIALOG_SUBTITLE}>
|
||||||
|
<span className="pr-2">Edit Dictionary</span>
|
||||||
|
<IconComponent
|
||||||
|
name="BookMarked"
|
||||||
|
className="h-6 w-6 pl-1 text-primary "
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</BaseModal.Header>
|
||||||
|
<BaseModal.Content>
|
||||||
|
<div className="flex h-full w-full flex-col transition-all">
|
||||||
|
<JsonView
|
||||||
|
theme="vscode"
|
||||||
|
dark={true}
|
||||||
|
editable
|
||||||
|
enableClipboard
|
||||||
|
onEdit={(edit) => {
|
||||||
|
setDictObj(edit["src"]);
|
||||||
|
}}
|
||||||
|
src={dictObj}
|
||||||
|
/>
|
||||||
|
<div className="flex h-fit w-full justify-end">
|
||||||
|
<Button
|
||||||
|
className="mt-3"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(dictObj);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseModal.Content>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1057,4 +1057,8 @@
|
||||||
.label-invalid{
|
.label-invalid{
|
||||||
@apply text-destructive
|
@apply text-destructive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-invalid{
|
||||||
|
@apply border-destructive focus:ring-destructive focus:border-destructive
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,21 @@ export type InputListComponentType = {
|
||||||
editNode?: boolean;
|
editNode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type KeyPairListComponentType = {
|
||||||
|
value: any;
|
||||||
|
onChange: (value: Object[]) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
editNode?: boolean;
|
||||||
|
duplicateKey?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DictComponentType = {
|
||||||
|
value: any;
|
||||||
|
onChange: (value) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
editNode?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type TextAreaComponentType = {
|
export type TextAreaComponentType = {
|
||||||
field_name?: string;
|
field_name?: string;
|
||||||
nodeClass?: APIClassType;
|
nodeClass?: APIClassType;
|
||||||
|
|
|
||||||
|
|
@ -204,30 +204,49 @@ export function validateNode(
|
||||||
node: { template },
|
node: { template },
|
||||||
} = node.data;
|
} = node.data;
|
||||||
|
|
||||||
return Object.keys(template).reduce(
|
return Object.keys(template).reduce((errors: Array<string>, t) => {
|
||||||
(errors: Array<string>, t) =>
|
if (
|
||||||
errors.concat(
|
template[t].required &&
|
||||||
template[t].required &&
|
template[t].show &&
|
||||||
template[t].show &&
|
(template[t].value === undefined ||
|
||||||
(template[t].value === undefined ||
|
template[t].value === null ||
|
||||||
template[t].value === null ||
|
template[t].value === "") &&
|
||||||
template[t].value === "") &&
|
!reactFlowInstance
|
||||||
!reactFlowInstance
|
.getEdges()
|
||||||
.getEdges()
|
.some(
|
||||||
.some(
|
(edge) =>
|
||||||
(edge) =>
|
edge.targetHandle?.split("|")[1] === t &&
|
||||||
edge.targetHandle?.split("|")[1] === t &&
|
edge.targetHandle.split("|")[2] === node.id
|
||||||
edge.targetHandle.split("|")[2] === node.id
|
)
|
||||||
)
|
) {
|
||||||
? [
|
errors.push(
|
||||||
`${type} is missing ${
|
`${type} is missing ${
|
||||||
template.display_name || toNormalCase(template[t].name)
|
template.display_name || toNormalCase(template[t].name)
|
||||||
}.`,
|
}.`
|
||||||
]
|
);
|
||||||
: []
|
} else if (
|
||||||
),
|
template[t].type === "dict" &&
|
||||||
[] as string[]
|
template[t].required &&
|
||||||
);
|
template[t].show &&
|
||||||
|
(template[t].value !== undefined ||
|
||||||
|
template[t].value !== null ||
|
||||||
|
template[t].value !== "")
|
||||||
|
) {
|
||||||
|
if (hasDuplicateKeys(template[t].value))
|
||||||
|
errors.push(
|
||||||
|
`${type} (${
|
||||||
|
template.display_name || template[t].name
|
||||||
|
}) contains duplicate keys with the same values.`
|
||||||
|
);
|
||||||
|
if (hasEmptyKey(template[t].value))
|
||||||
|
errors.push(
|
||||||
|
`${type} (${
|
||||||
|
template.display_name || template[t].name
|
||||||
|
}) field must not be empty.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}, [] as string[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateNodes(reactFlowInstance: ReactFlowInstance) {
|
export function validateNodes(reactFlowInstance: ReactFlowInstance) {
|
||||||
|
|
@ -291,3 +310,58 @@ export function getConnectedNodes(
|
||||||
const targetId = edge.target;
|
const targetId = edge.target;
|
||||||
return nodes.filter((node) => node.id === targetId || node.id === sourceId);
|
return nodes.filter((node) => node.id === targetId || node.id === sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertObjToArray(singleObject) {
|
||||||
|
if (Array.isArray(singleObject)) return singleObject;
|
||||||
|
|
||||||
|
let arrConverted: any = [];
|
||||||
|
for (const key in singleObject) {
|
||||||
|
if (singleObject.hasOwnProperty(key)) {
|
||||||
|
const newObj = {};
|
||||||
|
newObj[key] = singleObject[key];
|
||||||
|
arrConverted.push(newObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arrConverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasDuplicateKeys(array) {
|
||||||
|
const keys = {};
|
||||||
|
for (const obj of array) {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (keys[key]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
keys[key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasEmptyKey(objArray) {
|
||||||
|
for (const obj of objArray) {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key) && key === "") {
|
||||||
|
return true; // Found an empty key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // No empty keys found
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertValuesToNumbers(arr) {
|
||||||
|
return arr.map((obj) => {
|
||||||
|
const newObj = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
let value = obj[key];
|
||||||
|
if (/\s/g.test(value)) {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
|
newObj[key] =
|
||||||
|
value === "" || isNaN(value) ? value.toString() : Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
ArrowUpToLine,
|
ArrowUpToLine,
|
||||||
Bell,
|
Bell,
|
||||||
|
BookMarked,
|
||||||
Check,
|
Check,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
|
@ -307,5 +308,6 @@ export const nodeIconsLucide: iconsType = {
|
||||||
UserCog2,
|
UserCog2,
|
||||||
Key,
|
Key,
|
||||||
Unplug,
|
Unplug,
|
||||||
|
BookMarked,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,4 @@
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from langflow.services.utils import get_settings_manager
|
|
||||||
|
|
||||||
|
|
||||||
# def test_hugging_face_hub(client: TestClient):
|
|
||||||
# response = client.get("api/v1/all")
|
|
||||||
# assert response.status_code == 200
|
|
||||||
# json_response = response.json()
|
|
||||||
# language_models = json_response["llms"]
|
|
||||||
|
|
||||||
# model = language_models["HuggingFaceHub"]
|
|
||||||
# template = model["template"]
|
|
||||||
|
|
||||||
# assert template["cache"] == {
|
|
||||||
# "required": False,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": False,
|
|
||||||
# "multiline": False,
|
|
||||||
# "password": False,
|
|
||||||
# "name": "cache",
|
|
||||||
# "type": "bool",
|
|
||||||
# "list": False,
|
|
||||||
# "advanced": False,
|
|
||||||
# }
|
|
||||||
# assert template["verbose"] == {
|
|
||||||
# "required": False,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": False,
|
|
||||||
# "multiline": False,
|
|
||||||
# "value": False,
|
|
||||||
# "password": False,
|
|
||||||
# "name": "verbose",
|
|
||||||
# "type": "bool",
|
|
||||||
# "list": False,
|
|
||||||
# "advanced": False,
|
|
||||||
# }
|
|
||||||
# assert template["client"] == {
|
|
||||||
# "required": False,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": False,
|
|
||||||
# "multiline": False,
|
|
||||||
# "password": False,
|
|
||||||
# "name": "client",
|
|
||||||
# "type": "Any",
|
|
||||||
# "list": False,
|
|
||||||
# "advanced": False,
|
|
||||||
# }
|
|
||||||
# assert template["repo_id"] == {
|
|
||||||
# "required": False,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": True,
|
|
||||||
# "multiline": False,
|
|
||||||
# "value": "gpt2",
|
|
||||||
# "password": False,
|
|
||||||
# "name": "repo_id",
|
|
||||||
# "type": "str",
|
|
||||||
# "list": False,
|
|
||||||
# "advanced": False,
|
|
||||||
# }
|
|
||||||
# assert template["task"] == {
|
|
||||||
# "required": True,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": True,
|
|
||||||
# "multiline": False,
|
|
||||||
# "password": False,
|
|
||||||
# "options": ["text-generation", "text2text-generation"],
|
|
||||||
# "name": "task",
|
|
||||||
# "type": "str",
|
|
||||||
# "list": True,
|
|
||||||
# "advanced": True,
|
|
||||||
# }
|
|
||||||
# assert template["model_kwargs"] == {
|
|
||||||
# "required": False,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": True,
|
|
||||||
# "multiline": False,
|
|
||||||
# "password": False,
|
|
||||||
# "name": "model_kwargs",
|
|
||||||
# "type": "code",
|
|
||||||
# "list": False,
|
|
||||||
# "advanced": True,
|
|
||||||
# }
|
|
||||||
# assert template["huggingfacehub_api_token"] == {
|
|
||||||
# "required": False,
|
|
||||||
# "placeholder": "",
|
|
||||||
# "show": True,
|
|
||||||
# "multiline": False,
|
|
||||||
# "password": True,
|
|
||||||
# "name": "huggingfacehub_api_token",
|
|
||||||
# "display_name": "HuggingFace Hub API Token",
|
|
||||||
# "type": "str",
|
|
||||||
# "list": False,
|
|
||||||
# "advanced": False,
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
def test_openai(client: TestClient, logged_in_headers):
|
def test_openai(client: TestClient, logged_in_headers):
|
||||||
|
|
@ -270,7 +177,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"password": False,
|
"password": False,
|
||||||
"name": "model_kwargs",
|
"name": "model_kwargs",
|
||||||
"type": "code",
|
"type": "dict",
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": True,
|
"advanced": True,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
|
@ -325,7 +232,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"password": False,
|
"password": False,
|
||||||
"name": "logit_bias",
|
"name": "logit_bias",
|
||||||
"type": "code",
|
"type": "dict",
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
|
@ -442,7 +349,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers):
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"password": False,
|
"password": False,
|
||||||
"name": "model_kwargs",
|
"name": "model_kwargs",
|
||||||
"type": "code",
|
"type": "dict",
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": True,
|
"advanced": True,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"password": False,
|
"password": False,
|
||||||
"name": "partial_variables",
|
"name": "partial_variables",
|
||||||
"type": "code",
|
"type": "dict",
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ def test_format_dict():
|
||||||
}
|
}
|
||||||
expected_output = {
|
expected_output = {
|
||||||
"field1": {
|
"field1": {
|
||||||
"type": "code", # Mapping type is replaced with dict which is replaced with code
|
"type": "dict[str, int]", # Mapping type is replaced with dict which is replaced with code
|
||||||
"required": False,
|
"required": False,
|
||||||
"list": False,
|
"list": False,
|
||||||
"show": False,
|
"show": False,
|
||||||
|
|
@ -249,7 +249,7 @@ def test_format_dict():
|
||||||
}
|
}
|
||||||
expected_output = {
|
expected_output = {
|
||||||
"field1": {
|
"field1": {
|
||||||
"type": "code",
|
"type": "Dict[str, int]",
|
||||||
"required": False,
|
"required": False,
|
||||||
"list": False,
|
"list": False,
|
||||||
"show": False,
|
"show": False,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue