merge branch chatImg

This commit is contained in:
cristhianzl 2024-06-07 12:11:40 -03:00
commit 9ce7ee31a9
60 changed files with 1056 additions and 511 deletions

View file

@ -4,7 +4,7 @@ You can run Langflow in `--backend-only` mode to expose your Langflow app as an
Start langflow in backend-only mode with `python3 -m langflow run --backend-only`.
The terminal prints ` Welcome to ⛓ Langflow `, and a blank window opens at `http://127.0.0.1:7864/all`.
The terminal prints ` Welcome to ⛓ Langflow `, and a blank window opens at `http://127.0.0.1:7864/all`.
Langflow will now serve requests to its API without the frontend running.
## Prerequisites
@ -42,7 +42,7 @@ Note the flow ID of `ef7e0554-69e5-4e3e-ab29-ee83bcd8d9ef`. You can find this ID
1. Stop Langflow with Ctrl+C.
2. Start langflow in backend-only mode with `python3 -m langflow run --backend-only`.
The terminal prints ` Welcome to ⛓ Langflow `, and a blank window opens at `http://127.0.0.1:7864/all`.
The terminal prints ` Welcome to ⛓ Langflow `, and a blank window opens at `http://127.0.0.1:7864/all`.
Langflow will now serve requests to its API.
3. Run the curl code you copied from the UI.
You should get a result like this:

View file

@ -168,9 +168,9 @@ async def build_vertex(
next_runnable_vertices,
top_level_vertices,
result_dict,
log_message,
params,
valid,
log_type,
artifacts,
vertex,
) = await graph.build_vertex(
lock=lock,
@ -180,22 +180,22 @@ async def build_vertex(
inputs_dict=inputs.model_dump() if inputs else {},
files=files,
)
log_obj = Log(message=vertex.artifacts_raw, type=vertex.artifacts_type)
result_data_response = ResultDataResponse(**result_dict.model_dump())
except Exception as exc:
logger.exception(f"Error building vertex: {exc}")
log_message = format_exception_message(exc)
log_type = type(exc).__name__
params = format_exception_message(exc)
valid = False
log_obj = Log(message=params, type="error")
result_data_response = ResultDataResponse(results={})
log_object = Log(message=log_message, type=log_type)
artifacts = {}
# If there's an error building the vertex
# we need to clear the cache
await chat_service.clear_cache(flow_id_str)
result_data_response.logs.append(log_object)
result_data_response.message = artifacts
result_data_response.logs.append(log_obj)
# Log the vertex build
if not vertex.will_stream:
@ -204,8 +204,9 @@ async def build_vertex(
flow_id=flow_id_str,
vertex_id=vertex_id,
valid=valid,
logs=result_data_response.logs,
params=params,
data=result_data_response,
artifacts=artifacts,
)
timedelta = time.perf_counter() - start_time
@ -231,6 +232,7 @@ async def build_vertex(
next_vertices_ids=next_runnable_vertices,
top_level_vertices=top_level_vertices,
valid=valid,
params=params,
id=vertex.id,
data=result_data_response,
)

View file

@ -2,6 +2,7 @@ from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing_extensions import TypedDict
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer
@ -244,10 +245,16 @@ class VerticesOrderResponse(BaseModel):
vertices_to_run: List[str]
class Log(TypedDict):
message: Union[dict, str]
type: str
class ResultDataResponse(BaseModel):
results: Optional[Any] = Field(default_factory=dict)
logs: List[Log | None] = Field(default_factory=list)
messages: List[ChatOutputResponse | None] = Field(default_factory=list)
message: Optional[Any] = Field(default_factory=dict)
artifacts: Optional[Any] = Field(default_factory=dict)
timedelta: Optional[float] = None
duration: Optional[str] = None
used_frozen_result: Optional[bool] = False
@ -259,6 +266,8 @@ class VertexBuildResponse(BaseModel):
next_vertices_ids: Optional[List[str]] = None
top_level_vertices: Optional[List[str]] = None
valid: bool
params: Optional[Any] = Field(default_factory=dict)
"""JSON string of the params."""
data: ResultDataResponse
"""Mapping of vertex ids to result dict containing the param name and result value."""
timestamp: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc))

View file

@ -1,10 +1,13 @@
import warnings
from typing import Optional, Union
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.language_models.llms import LLM
from langchain_core.load import load
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langflow.custom import CustomComponent
from langflow.schema.schema import Record
class LCModelComponent(CustomComponent):
@ -82,7 +85,7 @@ class LCModelComponent(CustomComponent):
return status_message
def get_chat_result(
self, runnable: BaseChatModel, stream: bool, input_value: str, system_message: Optional[str] = None
self, runnable: BaseChatModel, stream: bool, input_value: str | Record, system_message: Optional[str] = None
):
messages: list[Union[HumanMessage, SystemMessage]] = []
if not input_value and not system_message:
@ -90,7 +93,16 @@ class LCModelComponent(CustomComponent):
if system_message:
messages.append(SystemMessage(content=system_message))
if input_value:
messages.append(HumanMessage(content=input_value))
if isinstance(input_value, Record):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if "prompt" in input_value:
prompt = load(input_value.prompt)
runnable = prompt | runnable
else:
messages.append(input_value.to_lc_message())
else:
messages.append(HumanMessage(content=input_value))
if stream:
return runnable.stream(messages)
else:

View file

@ -1,9 +1,10 @@
import base64
from copy import deepcopy
from langchain_core.documents import Document
from langflow.schema import Record
from langflow.services.deps import get_storage_service
def record_to_string(record: Record) -> str:
@ -19,7 +20,7 @@ def record_to_string(record: Record) -> str:
return record.get_text()
def dict_values_to_string(d: dict) -> dict:
async def dict_values_to_string(d: dict) -> dict:
"""
Converts the values of a dictionary to strings.
@ -36,16 +37,43 @@ def dict_values_to_string(d: dict) -> dict:
if isinstance(value, list):
for i, item in enumerate(value):
if isinstance(item, Record):
d_copy[key][i] = record_to_string(item)
d_copy[key][i] = item.to_lc_message()
elif isinstance(item, Document):
d_copy[key][i] = document_to_string(item)
elif isinstance(value, Record):
d_copy[key] = record_to_string(value)
if "files" in value and value.files:
files = await get_file_paths(value.files)
value.files = files
d_copy[key] = value.to_lc_message()
elif isinstance(value, Document):
d_copy[key] = document_to_string(value)
return d_copy
async def get_file_paths(files: list[str]):
storage_service = get_storage_service()
file_paths = []
for file in files:
flow_id, file_name = file.split("/")
file_paths.append(storage_service.build_full_path(flow_id=flow_id, file_name=file_name))
return file_paths
async def get_files(
file_paths: str,
convert_to_base64: bool = False,
):
storage_service = get_storage_service()
file_objects = []
for file_path in file_paths:
flow_id, file_name = file_path.split("/")
file_object = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
if convert_to_base64:
file_object = base64.b64encode(file_object).decode("utf-8")
file_objects.append(file_object)
return file_objects
def document_to_string(document: Document) -> str:
"""
Convert a document to a string.

View file

@ -1,7 +1,9 @@
from langchain_core.prompts import PromptTemplate
from langchain_core.prompts import ChatPromptTemplate
from langflow.base.prompts.utils import dict_values_to_string
from langflow.custom import CustomComponent
from langflow.field_typing import Prompt, TemplateField, Text
from langflow.schema.schema import Record
class PromptComponent(CustomComponent):
@ -15,19 +17,14 @@ class PromptComponent(CustomComponent):
"code": TemplateField(advanced=True),
}
def build(
async def build(
self,
template: Prompt,
**kwargs,
) -> Text:
from langflow.base.prompts.utils import dict_values_to_string
prompt_template = PromptTemplate.from_template(Text(template))
kwargs = dict_values_to_string(kwargs)
kwargs = {k: "\n".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}
try:
formated_prompt = prompt_template.format(**kwargs)
except Exception as exc:
raise ValueError(f"Error formatting prompt: {exc}") from exc
self.status = f'Prompt:\n"{formated_prompt}"'
return formated_prompt
) -> Record:
prompt_template = ChatPromptTemplate.from_template(Text(template))
kwargs = await dict_values_to_string(kwargs)
messages = list(kwargs.values())
prompt = prompt_template + messages
self.status = f'Prompt:\n"{template}"'
return Record(data={"prompt": prompt.to_json()})

View file

@ -738,7 +738,9 @@ class Graph:
# Check the cache for the vertex
cached_result = await chat_service.get_cache(key=vertex.id)
if isinstance(cached_result, CacheMiss):
await vertex.build(user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars)
await vertex.build(
user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars, files=files
)
await chat_service.set_cache(key=vertex.id, data=vertex)
else:
cached_vertex = cached_result["result"]
@ -752,7 +754,9 @@ class Graph:
vertex.result.used_frozen_result = True
else:
await vertex.build(user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars)
await vertex.build(
user_id=user_id, inputs=inputs_dict, fallback_to_env_vars=fallback_to_env_vars, files=files
)
if vertex.result is not None:
params = f"{vertex._built_object_repr()}{params}"

View file

@ -2,6 +2,7 @@ from enum import Enum
from typing import Any, Generator, Union
from langchain_core.documents import Document
from langflow.schema.schema import Record
from pydantic import BaseModel
from langflow.interface.utils import extract_input_variables_from_prompt

View file

@ -628,9 +628,8 @@ class Vertex:
self._built_object, self.artifacts = result
elif len(result) == 3:
self._custom_component, self._built_object, self.artifacts = result
self.artifacts_raw = self.artifacts.get("raw")
self.artifacts_type = self.artifacts.get("type") or ArtifactType.UNKNOWN.value
self.artifacts_raw = self.artifacts.pop("raw", None)
self.artifacts_type = self.artifacts.pop("type", None) or ArtifactType.UNKNOWN.value
else:
self._built_object = result

View file

@ -111,12 +111,7 @@ class InterfaceVertex(Vertex):
message = self._built_object
artifact_type = ArtifactType.STREAM if stream_url is not None else ArtifactType.OBJECT
artifacts = ChatOutputResponse(
message=message,
sender=sender,
sender_name=sender_name,
stream_url=stream_url,
files=files,
type=artifact_type.value,
message=message, sender=sender, sender_name=sender_name, stream_url=stream_url, files=files
)
self.will_stream = stream_url is not None
@ -213,9 +208,9 @@ class InterfaceVertex(Vertex):
flow_id=self.graph.flow_id,
vertex_id=self.id,
valid=True,
logs=self._built_object_repr(),
params=self._built_object_repr(),
data=self.result,
messages=self.artifacts,
artifacts=self.artifacts,
)
self._validate_built_object()

View file

@ -20,7 +20,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,

View file

@ -20,7 +20,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,

View file

@ -20,7 +20,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,

View file

@ -524,7 +524,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,

View file

@ -20,7 +20,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,
@ -130,7 +130,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,

View file

@ -1034,7 +1034,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from langchain_core.prompts import PromptTemplate\n\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Text:\n from langflow.base.prompts.utils import dict_values_to_string\n\n prompt_template = PromptTemplate.from_template(Text(template))\n kwargs = dict_values_to_string(kwargs)\n kwargs = {k: \"\\n\".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n try:\n formated_prompt = prompt_template.format(**kwargs)\n except Exception as exc:\n raise ValueError(f\"Error formatting prompt: {exc}\") from exc\n self.status = f'Prompt:\\n\"{formated_prompt}\"'\n return formated_prompt\n",
"value": "from langchain_core.prompts import ChatPromptTemplate\n\nfrom langflow.base.prompts.utils import dict_values_to_string\nfrom langflow.custom import CustomComponent\nfrom langflow.field_typing import Prompt, TemplateField, Text\nfrom langflow.schema.schema import Record\n\n\nclass PromptComponent(CustomComponent):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n\n def build_config(self):\n return {\n \"template\": TemplateField(display_name=\"Template\"),\n \"code\": TemplateField(advanced=True),\n }\n\n async def build(\n self,\n template: Prompt,\n **kwargs,\n ) -> Record:\n prompt_template = ChatPromptTemplate.from_template(Text(template))\n kwargs = await dict_values_to_string(kwargs)\n messages = list(kwargs.values())\n prompt = prompt_template + messages\n self.status = f'Prompt:\\n\"{template}\"'\n return Record(data={\"prompt\": prompt.to_json()})\n",
"fileTypes": [],
"file_path": "",
"password": false,

View file

@ -125,7 +125,6 @@ async def instantiate_custom_component(params, user_id, vertex, fallback_to_env_
custom_repr = build_result
if not isinstance(custom_repr, str):
custom_repr = str(custom_repr)
raw = custom_component.repr_value
if hasattr(raw, "data"):
raw = raw.data

View file

@ -1,11 +1,12 @@
import copy
import json
from typing import Literal, Optional, cast
from typing_extensions import TypedDict
from langchain_core.documents import Document
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from pydantic import BaseModel, model_validator
from typing_extensions import TypedDict
from langchain_core.prompts.image import ImagePromptTemplate
from pydantic import BaseModel, model_serializer, model_validator
class Record(BaseModel):
@ -30,6 +31,11 @@ class Record(BaseModel):
values["data"][key] = values[key]
return values
@model_serializer(mode="plain", when_used="json")
def serialize_model(self):
data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()}
return data
def get_text(self):
"""
Retrieves the text value from the data dictionary.
@ -103,7 +109,9 @@ class Record(BaseModel):
text = self.data.pop(self.text_key, self.default_value)
return Document(page_content=text, metadata=self.data)
def to_lc_message(self) -> BaseMessage:
def to_lc_message(
self,
) -> BaseMessage:
"""
Converts the Record to a BaseMessage.
@ -119,8 +127,22 @@ class Record(BaseModel):
raise ValueError(f"Missing required keys ('text', 'sender') in Record: {self.data}")
sender = self.data.get("sender", "Machine")
text = self.data.get("text", "")
files = self.data.get("files", [])
if sender == "User":
return HumanMessage(content=text)
if files:
contents = [{"type": "text", "text": text}]
for file_path in files:
image_template = ImagePromptTemplate()
image_prompt_value = image_template.invoke(input={"path": file_path})
contents.append({"type": "image_url", "image_url": image_prompt_value.image_url})
human_message = HumanMessage(content=contents)
else:
human_message = HumanMessage(
content=[{"type": "text", "text": text}],
)
return human_message
return AIMessage(content=text)
def __getattr__(self, key):
@ -170,8 +192,14 @@ class Record(BaseModel):
def __str__(self) -> str:
# return a JSON string representation of the Record atributes
try:
data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()}
return json.dumps(data, indent=4)
except Exception:
return str(self.data)
return json.dumps(self.data)
def __contains__(self, key):
return key in self.data
INPUT_FIELD_NAME = "input_value"

View file

@ -23,6 +23,9 @@ class CacheMiss:
def __repr__(self):
return "<CACHE_MISS>"
def __bool__(self):
return False
def create_cache_folder(func):
def wrapper(*args, **kwargs):

View file

@ -76,6 +76,7 @@ class MessageModel(BaseModel):
session_id: str
message: str
files: list[str] = []
artifacts: dict
class Config:
from_attributes = True
@ -87,6 +88,12 @@ class MessageModel(BaseModel):
return json.loads(v)
return v
@field_validator("artifacts", mode="before")
def validate_target_args(cls, v):
if isinstance(v, str):
return json.loads(v)
return v
@classmethod
def from_record(cls, record: "Record", flow_id: Optional[str] = None):
# first check if the record has all the required fields
@ -107,6 +114,12 @@ class MessageModel(BaseModel):
class MessageModelResponse(MessageModel):
index: Optional[int] = Field(default=None)
@field_validator("artifacts", mode="before")
def serialize_artifacts(v):
if isinstance(v, str):
return json.loads(v)
return v
@field_validator("index", mode="before")
def validate_id(cls, v):
if isinstance(v, float):
@ -129,15 +142,16 @@ class VertexBuildModel(BaseModel):
id: Optional[str] = Field(default=None, alias="id")
flow_id: str
valid: bool
logs: Any
params: Any
data: dict
artifacts: dict
timestamp: datetime = Field(default_factory=datetime.now)
class Config:
from_attributes = True
populate_by_name = True
@field_serializer("data")
@field_serializer("data", "artifacts")
def serialize_dict(v):
if isinstance(v, dict):
# check if the value of each key is a BaseModel or a list of BaseModels
@ -151,8 +165,8 @@ class VertexBuildModel(BaseModel):
return v.model_dump_json()
return v
@field_validator("logs", mode="before")
def validate_logs(cls, v):
@field_validator("params", mode="before")
def validate_params(cls, v):
if isinstance(v, str):
try:
return json.loads(v)
@ -160,7 +174,7 @@ class VertexBuildModel(BaseModel):
return v
return v
@field_serializer("logs")
@field_serializer("params")
def serialize_params(v):
if isinstance(v, list) and all(isinstance(i, BaseModel) for i in v):
return json.dumps([i.model_dump() for i in v])
@ -172,11 +186,17 @@ class VertexBuildModel(BaseModel):
return json.loads(v)
return v
@field_validator("artifacts", mode="before")
def validate_artifacts(cls, v):
if isinstance(v, str):
return json.loads(v)
elif isinstance(v, BaseModel):
return v.model_dump()
return v
class VertexBuildResponseModel(VertexBuildModel):
messages: list[MessageModel] = []
@field_serializer("data")
@field_serializer("data", "artifacts")
def serialize_dict(v):
return v

View file

@ -147,9 +147,9 @@ async def log_vertex_build(
flow_id: str,
vertex_id: str,
valid: bool,
logs: Any,
params: Any,
data: "ResultDataResponse",
messages: Optional[dict] = None,
artifacts: Optional[dict] = None,
):
try:
monitor_service = get_monitor_service()
@ -158,9 +158,9 @@ async def log_vertex_build(
"flow_id": flow_id,
"id": vertex_id,
"valid": valid,
"logs": logs,
"params": params,
"data": data.model_dump(),
"messages": messages or {},
"artifacts": artifacts or {},
"timestamp": monitor_service.get_timestamp(),
}
monitor_service.add_row(table_name="vertex_builds", data=row)

View file

@ -90,9 +90,9 @@ async def build_vertex(
flow_id=flow_id,
vertex_id=vertex_id,
valid=valid,
logs=params,
params=params,
data=result_dict,
messages=artifacts,
artifacts=artifacts,
)
# Emit the vertex build response

View file

@ -55,10 +55,10 @@ export default function GenericNode({
const setErrorData = useAlertStore((state) => state.setErrorData);
const isDark = useDarkStore((state) => state.dark);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status,
(state) => state.flowBuildStatus[data.id]?.status
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp,
(state) => state.flowBuildStatus[data.id]?.timestamp
);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
@ -66,7 +66,7 @@ export default function GenericNode({
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!,
data.node?.description!
);
const [isOutdated, setIsOutdated] = useState(false);
const [validationStatus, setValidationStatus] =
@ -84,7 +84,7 @@ export default function GenericNode({
data.node!,
setNode,
setIsOutdated,
updateNodeInternals,
updateNodeInternals
);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
@ -115,12 +115,12 @@ export default function GenericNode({
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: VertexBuildTypeAPI | null,
validationStatus: VertexBuildTypeAPI | null
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus,
isDark,
isDark
);
const baseBorderClass = getBaseBorderClass(selected);
@ -129,7 +129,7 @@ export default function GenericNode({
baseBorderClass,
nodeSizeClass,
"generic-node-div group/node",
specificClassFromBuildStatus,
specificClassFromBuildStatus
);
return names;
};
@ -170,7 +170,7 @@ export default function GenericNode({
showNode,
isEmoji,
nodeIconFragment,
checkNodeIconFragment,
checkNodeIconFragment
);
function countHandles(): void {
@ -247,7 +247,7 @@ export default function GenericNode({
selected,
showNode,
buildStatus,
validationStatus,
validationStatus
)}
>
{data.node?.beta && showNode && (
@ -378,7 +378,7 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField,
templateField
)}
info={data.node?.template[templateField].info}
name={templateField}
@ -406,7 +406,7 @@ export default function GenericNode({
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
),
)
)}
<ParameterComponent
key={scapedJSONStringfy({
@ -552,7 +552,7 @@ export default function GenericNode({
!data.node?.description) &&
nameEditable
? "font-light italic"
: "",
: ""
)}
onClick={(e) => {
setInputDescription(true);
@ -614,13 +614,13 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField,
templateField
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n",
"\n"
) ?? data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
@ -647,7 +647,7 @@ export default function GenericNode({
<div
className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center",
"flex-max-width justify-center"
)}
>
{" "}

View file

@ -32,7 +32,7 @@ const SideBarFoldersButtonsComponent = ({
const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false })),
folders.map((obj) => ({ name: obj.name, edit: false }))
);
const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/");
@ -59,7 +59,7 @@ const SideBarFoldersButtonsComponent = ({
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
folderId,
handleFolderChange,
handleFolderChange
);
const handleUploadFlowsToFolder = () => {
@ -87,7 +87,7 @@ const SideBarFoldersButtonsComponent = ({
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => {
getFoldersApi(true);
},
}
);
}
@ -133,7 +133,7 @@ const SideBarFoldersButtonsComponent = ({
<>
{folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
(folder) => folder.name === item.name
)[0];
return (
<div
@ -149,7 +149,7 @@ const SideBarFoldersButtonsComponent = ({
? "border border-border bg-muted hover:bg-muted"
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
folderIdDragging === item.id! ? "bg-border" : "",
folderIdDragging === item.id! ? "bg-border" : ""
)}
onClick={() => handleChangeFolder!(item.id!)}
>
@ -219,7 +219,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
}))
);
}
if (e.key === "Enter") {
@ -252,10 +252,10 @@ const SideBarFoldersButtonsComponent = ({
};
const updatedFolder = await updateFolder(
body,
item.id!,
item.id!
);
const updateFolders = folders.filter(
(f) => f.name !== item.name,
(f) => f.name !== item.name
);
setFolders([...updateFolders, updatedFolder]);
setFoldersNames({});
@ -263,7 +263,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
}))
);
} else {
setFoldersNames((old) => ({

View file

@ -63,8 +63,8 @@ export default function TableNodeCellRender({
...id,
proxy: templateData.proxy,
}
: id,
),
: id
)
) ?? false;
function getCellType() {
switch (templateData.type) {
@ -135,7 +135,7 @@ export default function TableNodeCellRender({
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
templateValue?.length > 1 ? "my-3" : "",
templateValue?.length > 1 ? "my-3" : ""
)}
>
<KeypairListComponent

View file

@ -13,6 +13,7 @@ import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import ResetColumns from "./components/ResetColumns";
import resetGrid from "./utils/reset-grid-columns";
import { useParams } from "react-router-dom";
interface TableComponentProps extends AgGridReactProps {
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;

View file

@ -62,7 +62,7 @@ export async function sendAll(data: sendAllProps) {
}
export async function postValidateCode(
code: string,
code: string
): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code });
}
@ -77,7 +77,7 @@ export async function postValidateCode(
export async function postValidatePrompt(
name: string,
template: string,
frontend_node: APIClassType,
frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
@ -150,7 +150,7 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType,
updatedFlow: FlowType
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
@ -328,7 +328,7 @@ export async function getHealth() {
*
*/
export async function getBuildStatus(
flowId: string,
flowId: string
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
}
@ -341,7 +341,7 @@ export async function getBuildStatus(
*
*/
export async function postBuildInit(
flow: FlowType,
flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
}
@ -357,7 +357,7 @@ export async function postBuildInit(
*/
export async function uploadFile(
file: File,
id: string,
id: string
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
@ -366,7 +366,7 @@ export async function uploadFile(
export async function postCustomComponent(
code: string,
apiClass: APIClassType,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
@ -379,7 +379,7 @@ export async function postCustomComponentUpdate(
code: string,
template: APITemplateType,
field: string,
field_value: any,
field_value: any
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
@ -401,7 +401,7 @@ export async function onLogin(user: LoginType) {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
}
);
if (response.status === 200) {
@ -463,11 +463,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage(
skip: number,
limit: number,
limit: number
): Promise<Array<Users>> {
try {
const res = await api.get(
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
);
if (res.status === 200) {
return res.data;
@ -504,7 +504,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try {
const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`,
user,
user
);
if (res.status === 200) {
return res.data;
@ -578,7 +578,7 @@ export async function saveFlowStore(
last_tested_version?: string;
},
tags: string[],
publicFlow = false,
publicFlow = false
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
@ -707,7 +707,7 @@ export async function postStoreComponents(component: Component) {
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`,
`${BASE_URL_API}store/components/${component_id}`
);
if (res.status === 200) {
return res.data;
@ -722,7 +722,7 @@ export async function searchComponent(
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[],
tags?: string[]
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
@ -834,7 +834,7 @@ export async function updateFlowStore(
},
tags: string[],
publicFlow = false,
id: string,
id: string
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
@ -918,7 +918,7 @@ export async function deleteGlobalVariable(id: string) {
export async function updateGlobalVariable(
name: string,
value: string,
id: string,
id: string
) {
try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -937,7 +937,7 @@ export async function getVerticesOrder(
startNodeId?: string | null,
stopNodeId?: string | null,
nodes?: Node[],
Edges?: Edge[],
Edges?: Edge[]
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
@ -957,7 +957,7 @@ export async function getVerticesOrder(
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices`,
data,
config,
config
);
}
@ -965,18 +965,16 @@ export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string,
files?: string[],
files?: string[]
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
const data = input_value
? { inputs: { input_value: input_value } }
: undefined;
const data = { inputs: { input_value: input_value ?? "" } };
if (data && files) {
data["files"] = files;
}
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
data,
data
);
}
@ -1000,7 +998,7 @@ export async function getFlowPool({
}
export async function deleteFlowPool(
flowId: string,
flowId: string
): Promise<AxiosResponse<any>> {
const config = {};
config["params"] = { flow_id: flowId };
@ -1014,7 +1012,7 @@ export async function deleteFlowPool(
* @returns A promise that resolves to an array of AxiosResponse objects representing the delete responses.
*/
export async function multipleDeleteFlowsComponents(
flowIds: string[],
flowIds: string[]
): Promise<AxiosResponse<any>[]> {
const batches: string[][] = [];
@ -1037,7 +1035,7 @@ export async function multipleDeleteFlowsComponents(
// Execute all delete requests
const responses: Promise<AxiosResponse<any>>[] = batches.map((batch) =>
deleteBatch(batch),
deleteBatch(batch)
);
// Return the responses after all requests are completed
@ -1047,7 +1045,7 @@ export async function multipleDeleteFlowsComponents(
export async function getTransactionTable(
id: string,
mode: "intersection" | "union",
params = {},
params = {}
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
@ -1063,7 +1061,7 @@ export async function getMessagesTable(
mode: "intersection" | "union",
id?: string,
excludedFields?: string[],
params = {},
params = {}
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
if (id) {

View file

@ -0,0 +1,52 @@
import IconComponent from "../../../../../../../components/genericIconComponent";
import { Case } from "../../../../../../../shared/components/caseComponent";
import { classNames } from "../../../../../../../utils/utils";
const ButtonSendWrapper = ({
send,
lockChat,
noInput,
saveLoading,
chatValue,
}) => {
return (
<button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background",
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
>
<Case condition={lockChat || saveLoading}>
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
</Case>
<Case condition={noInput}>
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
</Case>
<Case condition={!(lockChat || saveLoading) && !noInput}>
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
</Case>
</button>
);
};
export default ButtonSendWrapper;

View file

@ -0,0 +1,81 @@
import { Textarea } from "../../../../../../../components/ui/textarea";
import { classNames } from "../../../../../../../utils/utils";
const TextAreaWrapper = ({
checkSendingOk,
send,
lockChat,
noInput,
saveLoading,
chatValue,
setChatValue,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
inputRef,
setInputFocus,
files,
isDragging,
}) => {
const getPlaceholderText = (
isDragging: boolean,
noInput: boolean,
): string => {
if (isDragging) {
return "Drop here";
} else if (noInput) {
return CHAT_INPUT_PLACEHOLDER;
} else {
return CHAT_INPUT_PLACEHOLDER_SEND;
}
};
const lockClass =
lockChat || saveLoading
? "form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: "form-modal-lock-false bg-background";
const fileClass =
files.length > 0
? "rounded-b-lg ring-0 focus:ring-0 focus:border-2 rounded-t-none border-t-0 border-border focus:border-t-0 focus:border-ring"
: "rounded-md border-t border-border focus:ring-0 focus:border-2 focus:border-ring";
const additionalClassNames = "form-modal-lockchat pl-14";
return (
<Textarea
onFocus={(e) => {
setInputFocus(true);
e.target.style.borderTopWidth = "0";
}}
onBlur={() => setInputFocus(false)}
onKeyDown={(event) => {
if (checkSendingOk(event)) {
send();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput || saveLoading}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(lockClass, fileClass, additionalClassNames)}
placeholder={getPlaceholderText(isDragging, noInput)}
/>
);
};
export default TextAreaWrapper;

View file

@ -0,0 +1,29 @@
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
const UploadFileButton = ({
fileInputRef,
handleFileChange,
handleButtonClick,
}) => {
return (
<div>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Button
className="font-bold text-white transition-all hover:text-muted-foreground"
onClick={handleButtonClick}
variant="none"
size="none"
>
<ForwardedIconComponent name="PaperclipIcon" />
</Button>
</div>
);
};
export default UploadFileButton;

View file

@ -0,0 +1,7 @@
export const getClassNamesFilePreview = (inputFocus) => {
return `flex w-full items-center gap-4 rounded-t-lg bg-background px-14 py-5 overflow-auto custom-scroll ${
inputFocus
? "border border-b-0 border-ring border-2"
: "border border-b-0 border-border"
}`;
};

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
const useAutoResizeTextArea = (value, inputRef) => {
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
inputRef.current.style!.height = "inherit"; // Reset the height
inputRef.current.style!.height = `${inputRef.current.scrollHeight!}px`; // Set it to the scrollHeight
}
}, [value]);
return inputRef;
};
export default useAutoResizeTextArea;

View file

@ -0,0 +1,55 @@
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
const useDragAndDrop = (setIsDragging, setFiles, currentFlowId) => {
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((type) => type === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((type) => type === "Files")) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const onDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files, setFiles, currentFlowId);
e.dataTransfer.clearData();
}
setIsDragging(false);
};
return {
dragOver,
dragEnter,
dragLeave,
onDrop,
};
};
const handleFiles = (files, setFiles, currentFlowId) => {
if (files) {
const uid = new ShortUniqueId({ length: 3 });
const id = uid();
const type = files[0].type.split("/")[0];
const blob = files[0];
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
};
export default useDragAndDrop;

View file

@ -0,0 +1,27 @@
import { uploadFile } from "../../../../../../controllers/API";
const useFileUpload = (blob, currentFlowId, setFiles, id) => {
uploadFile(blob, currentFlowId)
.then((res) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = res.data.file_path;
return newFiles;
});
})
.catch(() => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
});
return null;
};
export default useFileUpload;

View file

@ -0,0 +1,13 @@
import { useEffect } from "react";
const useFocusOnUnlock = (lockChat, inputRef) => {
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
return inputRef;
};
export default useFocusOnUnlock;

View file

@ -0,0 +1,33 @@
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
export const useHandleFileChange = (setFiles, currentFlowId) => {
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const fileInput = event.target;
const file = fileInput.files?.[0];
if (file) {
const uid = new ShortUniqueId({ length: 10 }); // Increase the length to ensure uniqueness
const id = uid();
const type = file.type.split("/")[0];
const blob = file;
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
// Clear the file input value to ensure the change event is triggered even for the same file
fileInput.value = "";
};
return {
handleFileChange,
};
};
export default useHandleFileChange;

View file

@ -0,0 +1,35 @@
import { useEffect } from "react";
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
const useUpload = (uploadFile, currentFlowId, setFiles) => {
useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => {
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const type = items[0].type.split("/")[0];
const uid = new ShortUniqueId({ length: 3 });
const blob = items[i].getAsFile();
if (blob) {
const id = uid();
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
}
}
};
document.addEventListener("paste", handlePaste);
return () => {
document.removeEventListener("paste", handlePaste);
};
}, [uploadFile, currentFlowId]);
return null;
};
export default useUpload;

View file

@ -1,7 +1,4 @@
import { useEffect, useState } from "react";
import ShortUniqueId from "short-unique-id";
import IconComponent from "../../../../../components/genericIconComponent";
import { Textarea } from "../../../../../components/ui/textarea";
import { useRef, useState } from "react";
import {
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
@ -9,11 +6,18 @@ import {
import { uploadFile } from "../../../../../controllers/API";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import {
ChatInputType,
FilePreviewType,
chatInputType,
} from "../../../../../types/components";
import { classNames } from "../../../../../utils/utils";
import FilePreview from "../filePreviewChat";
import ButtonSendWrapper from "./components/buttonSendWrapper";
import TextAreaWrapper from "./components/textAreaWrapper";
import UploadFileButton from "./components/uploadFileButton";
import { getClassNamesFilePreview } from "./helpers/get-class-file-preview";
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
import useFocusOnUnlock from "./hooks/use-focus-unlock";
import useHandleFileChange from "./hooks/use-handle-file-change";
import useUpload from "./hooks/use-upload";
export default function ChatInput({
lockChat,
chatValue,
@ -21,199 +25,99 @@ export default function ChatInput({
setChatValue,
inputRef,
noInput,
}: chatInputType): JSX.Element {
files,
setFiles,
isDragging,
}: ChatInputType): JSX.Element {
const [repeat, setRepeat] = useState(1);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const uid = new ShortUniqueId({ length: 3 });
const [files, setFiles] = useState<FilePreviewType[]>([]);
const [inputFocus, setInputFocus] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
useFocusOnUnlock(lockChat, inputRef);
useAutoResizeTextArea(chatValue, inputRef);
useUpload(uploadFile, currentFlowId, setFiles);
const { handleFileChange } = useHandleFileChange(setFiles, currentFlowId);
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight !== 0) {
inputRef.current.style.height = "inherit"; // Reset the height
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
}
}, [chatValue]);
//listen to ctrl-v to paste images
useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => {
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const blob = items[i].getAsFile();
if (blob) {
const id = uid();
setFiles([
...files,
{ file: blob, loading: true, error: false, id },
]);
uploadFile(blob, currentFlowId)
.then((res) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex(
(file) => file.id === id,
);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = res.data.file_path;
return newFiles;
});
})
.catch((err) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex(
(file) => file.id === id,
);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
});
}
}
}
}
};
document.addEventListener("paste", handlePaste);
return () => {
document.removeEventListener("paste", handlePaste);
};
}, []);
function send() {
const send = () => {
sendMessage({
repeat,
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
});
setFiles([]);
}
};
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
return (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
);
};
const classNameFilePreview = getClassNamesFilePreview(inputFocus);
const handleButtonClick = () => {
fileInputRef.current!.click();
};
return (
<div className="flex w-full flex-col-reverse">
<div className="relative w-full">
<Textarea
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
send();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput || saveLoading}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={
lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue
}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(
lockChat || saveLoading
? " form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: " form-modal-lock-false bg-background",
"form-modal-lockchat",
)}
placeholder={
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
}
<TextAreaWrapper
checkSendingOk={checkSendingOk}
send={send}
lockChat={lockChat}
noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue}
setChatValue={setChatValue}
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
inputRef={inputRef}
setInputFocus={setInputFocus}
files={files}
isDragging={isDragging}
/>
<div className="form-modal-send-icon-position">
<button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background",
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
>
{lockChat || saveLoading ? (
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
) : noInput ? (
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
) : (
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
)}
</button>
<ButtonSendWrapper
send={send}
lockChat={lockChat}
noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue}
/>
</div>
<div className="absolute bottom-2 left-4">
<UploadFileButton
fileInputRef={fileInputRef}
handleFileChange={handleFileChange}
handleButtonClick={handleButtonClick}
/>
</div>
</div>
<div className="flex w-full gap-2 pb-2">
{files.map((file) => (
<FilePreview
error={file.error}
file={file.file}
loading={file.loading}
key={file.id}
onDelete={() => {
setFiles((prev) => prev.filter((f) => f.id !== file.id));
// TODO: delete file on backend
}}
/>
))}
</div>
{/*
<Popover>
<PopoverTrigger asChild>
<Button variant="primary" className="h-13 px-4">
<IconComponent name="Repeat" className="" aria-hidden="true" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit">
<div className="flex flex-col items-center justify-center gap-2">
<span className="text-sm">Repetitions: </span>
<Input
onChange={(e) => {
handleChange(parseInt(e.target.value));
{files.length > 0 && (
<div className={classNameFilePreview}>
{files.map((file) => (
<FilePreview
error={file.error}
file={file.file}
loading={file.loading}
key={file.id}
onDelete={() => {
setFiles((prev: FilePreviewType[]) =>
prev.filter((f) => f.id !== file.id)
);
// TODO: delete file on backend
}}
className="w-16"
type="number"
min={0}
/>
</div>
</PopoverContent>
</Popover> */}
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,35 @@
import { useState } from "react";
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import formatFileName from "../../../filePreviewChat/utils/format-file-name";
import FileCard from "../../../fileComponent";
export default function FileCardWrapper({
index,
name,
type,
path,
}: {
index: number;
name: string;
type: string;
path: string;
}) {
const [show, setShow] = useState<boolean>(true);
return (
<div key={index} className="flex flex-col gap-2">
<span
onClick={() => setShow(!show)}
className="flex cursor-pointer gap-2 text-sm text-muted-foreground"
>
{formatFileName(name, 50)}
<ForwardedIconComponent name={show ? "ChevronDown" : "ChevronRight"} />
</span>
<FileCard
showFile={show}
fileName={name}
fileType={type}
content={path}
/>
</div>
);
}

View file

@ -7,13 +7,17 @@ import remarkMath from "remark-math";
import MaleTechnology from "../../../../../assets/male-technologist.png";
import Robot from "../../../../../assets/robot.png";
import CodeTabsComponent from "../../../../../components/codeTabsComponent";
import IconComponent from "../../../../../components/genericIconComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/genericIconComponent";
import SanitizedHTMLWrapper from "../../../../../components/sanitizedHTMLWrapper";
import useAlertStore from "../../../../../stores/alertStore";
import useFlowStore from "../../../../../stores/flowStore";
import { chatMessagePropsType } from "../../../../../types/components";
import { classNames, cn } from "../../../../../utils/utils";
import FileCard from "../fileComponent";
import formatFileName from "../filePreviewChat/utils/format-file-name";
import FileCardWrapper from "./components/fileCardWrapper";
export default function ChatMessage({
chat,
@ -22,6 +26,7 @@ export default function ChatMessage({
updateChat,
setLockChat,
}: chatMessagePropsType): JSX.Element {
const [showFile, setShowFile] = useState<boolean>(true);
const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true);
const template = chat.template;
@ -308,34 +313,35 @@ dark:prose-invert"
</span>
</>
) : (
<span
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
>
{chatMessage}
</span>
<div className="flex flex-col">
<span
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
>
{chatMessage}
</span>
{chat.files && (
<div className="my-2 flex flex-col gap-5">
{chat.files.map((file, index) => {
return (
<FileCardWrapper
index={index}
name={file.name}
type={file.type}
path={file.path}
/>
);
})}
</div>
)}
</div>
)}
</div>
)}
</div>
<div id={lastMessage ? "last-chat-message" : ""}></div>
{chat.files && (
<div className="my-2 w-full">
{chat.files.map((file, index) => {
return (
<div key={index} className="my-2 w-full">
<FileCard
fileName={file.name}
fileType={file.type}
content={file.path}
/>
</div>
);
})}
</div>
)}
</>
);
}

View file

@ -0,0 +1,25 @@
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
export default function DownloadButton({
isHovered,
handleDownload,
}: {
isHovered: boolean;
handleDownload: () => void;
}): JSX.Element | undefined {
if (isHovered) {
return (
<div
className={`absolute right-1 top-1 rounded-bl-lg bg-muted text-sm font-bold text-foreground `}
>
<button className="px-2 py-1 text-ring " onClick={handleDownload}>
<ForwardedIconComponent
name="DownloadCloud"
className="h-5 w-5 text-current hover:scale-110"
/>
</button>
</div>
);
}
return undefined;
}

View file

@ -1,8 +1,12 @@
import { useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import { ForwardedIconComponent } from "../../../../../components/genericIconComponent";
import { BACKEND_URL, BASE_URL_API } from "../../../../../constants/constants";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import { fileCardPropsType } from "../../../../../types/components";
import formatFileName from "../filePreviewChat/utils/format-file-name";
import DownloadButton from "./components/downloadButton/downloadButton";
import getClasses from "./utils/get-classes";
import handleDownload from "./utils/handle-download";
const imgTypes = new Set(["png", "jpg"]);
@ -10,10 +14,8 @@ export default function FileCard({
fileName,
content,
fileType,
}: fileCardPropsType): JSX.Element {
const handleDownload = (): void => {
//TODO: update download function
};
showFile = true,
}: fileCardPropsType): JSX.Element | undefined {
const [isHovered, setIsHovered] = useState(false);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
function handleMouseEnter(): void {
@ -23,64 +25,57 @@ export default function FileCard({
setIsHovered(false);
}
if (imgTypes.has(fileType)) {
const fileWrapperClasses = getClasses(isHovered);
const imgSrc = `${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1
)}${BASE_URL_API}files/images/${content}`;
if (showFile) {
if (imgTypes.has(fileType)) {
return (
<div
className="inline-block w-full rounded-lg transition-all"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ display: "inline-block" }}
>
<div className="relative w-[50%] rounded-lg border border-border">
<img
src={imgSrc}
alt="generated image"
className="m-0 h-auto w-auto rounded-lg border border-border p-0 transition-all"
/>
<DownloadButton
isHovered={isHovered}
handleDownload={() => handleDownload({ fileName, content })}
/>
</div>
</div>
);
}
return (
<div
className="relative h-1/4 w-1/4"
className={fileWrapperClasses}
onClick={() => handleDownload({ fileName, content })}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<img
src={`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1,
)}${BASE_URL_API}files/images/${content}`}
alt="generated image"
className="h-full w-full rounded-lg"
/>
{isHovered && (
<div className={`file-card-modal-image-div `}>
<button
className="file-card-modal-image-button "
onClick={handleDownload}
>
<IconComponent
name="DownloadCloud"
className="h-5 w-5 text-current hover:scale-110"
/>
</button>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(fileName, 20)}</span>
<span>File</span>
</div>
)}
</div>
<DownloadButton
isHovered={isHovered}
handleDownload={() => handleDownload({ fileName, content })}
/>
</div>
);
}
return (
<button onClick={handleDownload} className="file-card-modal-button">
<div className="file-card-modal-div">
{" "}
{imgTypes.has(fileType) ? (
<img
src={`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1,
)}${BASE_URL_API}files/images/${content}`}
alt=""
className="h-8 w-8"
/>
) : (
<IconComponent name="File" className="h-8 w-8" />
)}
<div className="file-card-modal-footer">
{" "}
<div className="file-card-modal-name">{fileName}</div>
<div className="file-card-modal-type">{fileType}</div>
</div>
<IconComponent
name="DownloadCloud"
className="ml-auto h-6 w-6 text-current"
/>
</div>
</button>
);
return undefined;
}

View file

@ -0,0 +1,5 @@
export default function getClasses(isHovered: boolean): string {
return `relative ${false ? "h-20 w-20" : "h-20 w-80"} cursor-pointer rounded-lg border border-ring bg-muted shadow transition duration-300 hover:drop-shadow-lg ${
isHovered ? "shadow-md" : ""
}`;
}

View file

@ -0,0 +1,43 @@
import {
BACKEND_URL,
BASE_URL_API,
} from "../../../../../../constants/constants";
let isDownloading = false;
export default async function handleDownload({
fileName,
content,
}: {
fileName: string;
content: string;
}): Promise<void> {
if (isDownloading) return;
try {
isDownloading = true;
const response = await fetch(
`${BACKEND_URL.slice(0, BACKEND_URL.length - 1)}${BASE_URL_API}files/download/${content}`,
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName); // Set the filename
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up the URL object
} catch (error) {
console.error("Failed to download file:", error);
} finally {
isDownloading = false;
}
}

View file

@ -1,6 +1,9 @@
import { useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import LoadingComponent from "../../../../../components/loadingComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/genericIconComponent";
import { Skeleton } from "../../../../../components/ui/skeleton";
import formatFileName from "./utils/format-file-name";
export default function FilePreview({
error,
@ -13,35 +16,92 @@ export default function FilePreview({
error: boolean;
onDelete: () => void;
}) {
const isImage = file.type.toLowerCase().includes("image");
const [isHovered, setIsHovered] = useState(false);
return (
<div className="relative inline-block w-56">
{loading && <LoadingComponent remSize={5} />}
{error && <div>Error...</div>}
<div
className={`relative overflow-hidden rounded-md bg-background p-4 transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<img
src={URL.createObjectURL(file)}
alt="file"
className="block h-auto w-full"
/>
{isHovered && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
<div
className="cursor-pointer rounded-full bg-white bg-opacity-80 p-2"
onClick={onDelete}
<div className="relative inline-block">
{loading ? (
isImage ? (
<div className="flex h-20 w-20 items-center justify-center rounded-md border border-ring bg-background ">
<svg
aria-hidden="true"
className={`h-10 w-10 animate-spin fill-black text-muted`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<IconComponent name="trash" className="stroke-red-500" />
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
) : (
<div
className={`relative ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-wait rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-48" />
<Skeleton className="h-3 w-10" />
</div>
</div>
</div>
)}
</div>
)
) : error ? (
<div>Error...</div>
) : (
<div
className={`relative mt-2 ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-pointer rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isImage ? (
<img
src={URL.createObjectURL(file)}
alt="file"
className="block h-full w-full rounded-md border border-border"
/>
) : (
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(file.name)}</span>
<span>File</span>
</div>
</div>
)}
{isHovered && (
<div
className={`absolute ${
isImage ? "bottom-16 left-16" : "bottom-16 left-[19em]"
} flex h-5 w-5 items-center justify-center`}
>
<div
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-gray-200 p-2 transition-all"
onClick={onDelete}
>
<IconComponent name="X" className="stroke-slate-950 stroke-2" />
</div>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,14 @@
export default function formatFileName(
name: string,
numberToTruncate: number = 25,
): string {
if (name[numberToTruncate] === undefined) {
return name;
}
const fileExtension = name.split(".").pop(); // Get the file extension
const baseName = name.slice(0, name.lastIndexOf(".")); // Get the base name without the extension
if (baseName.length > 6) {
return `${baseName.slice(0, numberToTruncate)}...${fileExtension}`;
}
return name;
}

View file

@ -11,9 +11,10 @@ import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
import { chatViewProps } from "../../../../types/components";
import { FilePreviewType, chatViewProps } from "../../../../types/components";
import { classNames } from "../../../../utils/utils";
import ChatInput from "./chatInput";
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
import ChatMessage from "./chatMessage";
export default function ChatView({
@ -61,13 +62,12 @@ export default function ChatView({
const chatMessages: ChatMessageType[] = chatOutputResponses
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
//
.filter(
(output) => output.data.messages && output.data.messages.length > 0,
)
.filter((output) => output.data.message)
.map((output, index) => {
try {
const { sender, message, sender_name, stream_url, files } = output
.data.messages[0] as ChatOutputType;
.data.message as ChatOutputType;
const is_ai = sender === "Machine" || sender === null;
return {
isSend: !is_ai,
@ -131,7 +131,7 @@ export default function ChatView({
function updateChat(
chat: ChatMessageType,
message: string,
stream_url?: string,
stream_url?: string
) {
// if (message === "") return;
chat.message = message;
@ -154,9 +154,23 @@ export default function ChatView({
// return newChatHistory;
// });
}
const [files, setFiles] = useState<FilePreviewType[]>([]);
const [isDragging, setIsDragging] = useState(false);
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
setIsDragging,
setFiles,
currentFlowId
);
return (
<div className="eraser-column-arrangement">
<div
className="eraser-column-arrangement"
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
>
<div className="eraser-size">
<div className="eraser-position">
<Button
@ -258,6 +272,9 @@ export default function ChatView({
setChatValue(value);
}}
inputRef={ref}
files={files}
setFiles={setFiles}
isDragging={isDragging}
/>
</div>
</div>

View file

@ -21,6 +21,7 @@ import { Button } from "../../components/ui/button";
import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
import { switchCaseModalSize } from "./helpers/switch-case-size";
import * as Form from "@radix-ui/react-form";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };

View file

@ -8,7 +8,7 @@ const useColumnDefs = (
myData: any,
handleOnNewValue: (newValue: any, name: string) => void,
changeAdvanced: (n: string) => void,
open: boolean,
open: boolean
) => {
const columnDefs: ColDef[] = useMemo(
() => [
@ -81,7 +81,7 @@ const useColumnDefs = (
cellClass: "no-border",
},
],
[open, myData],
[open, myData]
);
return columnDefs;

View file

@ -22,7 +22,7 @@ const EditNodeModal = forwardRef(
setOpen: (open: boolean) => void;
data: NodeDataType;
},
ref,
ref
) => {
const myData = useRef(data);
@ -43,7 +43,7 @@ const EditNodeModal = forwardRef(
data,
handleOnNewValue,
changeAdvanced,
open,
open
);
const [gridApi, setGridApi] = useState<GridApi | null>(null);
@ -107,7 +107,7 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer submit={{ label: "Save Changes" }} />
</BaseModal>
);
},
}
);
export default EditNodeModal;

View file

@ -40,22 +40,22 @@ export default function ComponentsComponent({
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const flowsFromFolder = useFolderStore(
(state) => state.selectedFolder?.flows,
(state) => state.selectedFolder?.flows
);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [openDelete, setOpenDelete] = useState(false);
const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents,
(state) => state.searchFlowsComponents
);
const setSelectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.setSelectedFlowsComponentsCards,
(state) => state.setSelectedFlowsComponentsCards
);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
(state) => state.selectedFlowsComponentsCards
);
const [handleFileDrop] = useFileDrop(uploadFlow, type)!;
@ -109,7 +109,7 @@ export default function ComponentsComponent({
const { handleSelectAll } = useSelectAll(
flowsFromFolder,
getValues,
setValue,
setValue
);
const { handleDuplicate } = useDuplicateFlows(
@ -124,7 +124,7 @@ export default function ComponentsComponent({
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes,
cardTypes
);
const version = useDarkStore((state) => state.version);
@ -138,7 +138,7 @@ export default function ComponentsComponent({
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes,
cardTypes
);
const { handleSelectOptionsChange } = useSelectOptionsChange(
@ -146,7 +146,7 @@ export default function ComponentsComponent({
setErrorData,
setOpenDelete,
handleDuplicate,
handleExport,
handleExport
);
const { handleDeleteMultiple } = useDeleteMultipleFlows(
@ -158,21 +158,21 @@ export default function ComponentsComponent({
myCollectionId,
getFolderById,
setSuccessData,
setErrorData,
setErrorData
);
useSelectedFlows(entireFormValues, setSelectedFlowsComponentsCards);
const descriptionModal = useDescriptionModal(
selectedFlowsComponentsCards,
type,
type
);
const getTotalRowsCount = () => {
if (type === "all") return allFlows?.length;
return allFlows?.filter(
(f) => (f.is_component ?? false) === (type === "component"),
(f) => (f.is_component ?? false) === (type === "component")
)?.length;
};

View file

@ -1,9 +1,12 @@
import { useState } from "react";
import IconComponent from "../../../../components/genericIconComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent";
import { Button } from "../../../../components/ui/button";
import { Checkbox } from "../../../../components/ui/checkbox";
import { cn } from "../../../../utils/utils";
import { Button } from "../../../../components/ui/button";
type HeaderComponentProps = {
handleSelectAll: (select) => void;

View file

@ -25,7 +25,7 @@ export default function ApiKeysPage() {
userData,
setLoadingKeys,
keysList,
setUserId,
setUserId
);
function resetFilter() {
@ -36,7 +36,7 @@ export default function ApiKeysPage() {
selectedRows,
resetFilter,
setSuccessData,
setErrorData,
setErrorData
);
const columnDefs = getColumnDefs();

View file

@ -16,18 +16,14 @@ import {
import { BuildStatus } from "../constants/enums";
import { getFlowPool } from "../controllers/API";
import { VertexBuildTypeAPI } from "../types/api";
import { ChatInputType, ChatOutputType } from "../types/chat";
import {
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import {
ChatOutputType,
FlowStoreType,
VertexLayerElementType,
chatInputType,
} from "../types/zustand/flow";
import { FlowStoreType, VertexLayerElementType } from "../types/zustand/flow";
import { buildVertices } from "../utils/buildUtils";
import {
checkChatInput,
@ -77,8 +73,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
},
updateFlowPool: (
nodeId: string,
data: VertexBuildTypeAPI | ChatOutputType | chatInputType,
buildId?: string,
data: VertexBuildTypeAPI | ChatOutputType | ChatInputType,
buildId?: string
) => {
let newFlowPool = cloneDeep({ ...get().flowPool });
if (!newFlowPool[nodeId]) {
@ -94,9 +90,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}
//update data results
else {
newFlowPool[nodeId][index].data.messages[0] = data as
newFlowPool[nodeId][index].data.message = data as
| ChatOutputType
| chatInputType;
| ChatInputType;
}
}
get().setFlowPool(newFlowPool);
@ -171,7 +167,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
flowsManager.autoSaveCurrentFlow(
newChange,
newEdges,
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 },
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 }
);
}
},
@ -187,7 +183,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
flowsManager.autoSaveCurrentFlow(
get().nodes,
newChange,
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 },
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 }
);
}
},
@ -205,7 +201,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
return newChange;
}
return node;
}),
})
);
},
getNode: (id: string) => {
@ -216,8 +212,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
get().nodes.filter((node) =>
typeof nodeId === "string"
? node.id !== nodeId
: !nodeId.includes(node.id),
),
: !nodeId.includes(node.id)
)
);
},
deleteEdge: (edgeId) => {
@ -225,8 +221,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
get().edges.filter((edge) =>
typeof edgeId === "string"
? edge.id !== edgeId
: !edgeId.includes(edge.id),
),
: !edgeId.includes(edge.id)
)
);
},
paste: (selection, position) => {
@ -292,7 +288,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
let source = idsMap[edge.source];
let target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!,
edge.sourceHandle!
);
let sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
@ -302,7 +298,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!,
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
@ -323,7 +319,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
className: "stroke-gray-900 ",
selected: false,
},
newEdges.map((edge) => ({ ...edge, selected: false })),
newEdges.map((edge) => ({ ...edge, selected: false }))
);
});
get().setEdges(newEdges);
@ -342,10 +338,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
const newNodes = get().nodes.filter(
(node) => !nodesIdsSelected.includes(node.id),
(node) => !nodesIdsSelected.includes(node.id)
);
const newEdges = get().edges.filter(
(edge) => !edgesIdsSelected.includes(edge.id),
(edge) => !edgesIdsSelected.includes(edge.id)
);
set({ nodes: newNodes, edges: newEdges });
@ -403,7 +399,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
style: { stroke: "#555" },
className: "stroke-foreground stroke-connection",
},
oldEdges,
oldEdges
);
return newEdges;
@ -413,7 +409,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
.autoSaveCurrentFlow(
get().nodes,
newEdges,
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 },
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 }
);
},
unselectAll: () => {
@ -448,7 +444,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
function validateSubgraph(nodes: string[]) {
const errorsObjs = validateNodes(
get().nodes.filter((node) => nodes.includes(node.id)),
get().edges,
get().edges
);
const errors = errorsObjs.map((obj) => obj.errors).flat();
@ -467,13 +463,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
function handleBuildUpdate(
vertexBuildData: VertexBuildTypeAPI,
status: BuildStatus,
runId: string,
runId: string
) {
if (vertexBuildData && vertexBuildData.inactivated_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactivated_vertices);
get().updateBuildStatus(
vertexBuildData.inactivated_vertices,
BuildStatus.INACTIVE,
BuildStatus.INACTIVE
);
}
@ -489,15 +485,14 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
// next_vertices_ids should be next_vertices_ids without the inactivated vertices
const next_vertices_ids = vertexBuildData.next_vertices_ids.filter(
(id) => !vertexBuildData.inactivated_vertices?.includes(id),
(id) => !vertexBuildData.inactivated_vertices?.includes(id)
);
const top_level_vertices = vertexBuildData.top_level_vertices.filter(
(vertex) =>
!vertexBuildData.inactivated_vertices?.includes(vertex.id),
(vertex) => !vertexBuildData.inactivated_vertices?.includes(vertex)
);
const nextVertices: VertexLayerElementType[] = zip(
next_vertices_ids,
top_level_vertices,
top_level_vertices
).map(([id, reference]) => ({ id: id!, reference }));
const newLayers = [
@ -518,8 +513,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}
get().addDataToFlowPool(
{ ...vertexBuildData, buildId: runId },
vertexBuildData.id,
{ ...vertexBuildData, run_id: runId },
vertexBuildData.id
);
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
@ -528,7 +523,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const newFlowBuildStatus = { ...get().flowBuildStatus };
// filter out the vertices that are not status
const verticesToUpdate = verticesIds?.filter(
(id) => newFlowBuildStatus[id]?.status !== BuildStatus.BUILT,
(id) => newFlowBuildStatus[id]?.status !== BuildStatus.BUILT
);
if (verticesToUpdate) {
@ -598,7 +593,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
verticesLayers: VertexLayerElementType[][];
runId: string;
verticesToRun: string[];
} | null,
} | null
) => {
set({ verticesBuild: vertices });
},
@ -623,7 +618,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
// that are going to be built
verticesIds: get().verticesBuild!.verticesIds.filter(
// keep the vertices that are not in the list of vertices to remove
(vertex) => !vertices.includes(vertex),
(vertex) => !vertices.includes(vertex)
),
},
});

View file

@ -1,5 +1,5 @@
import { Edge, Node, Viewport } from "reactflow";
import { ChatOutputType, chatInputType } from "../chat";
import { ChatInputType, ChatOutputType } from "../chat";
import { FlowType } from "../flow";
//kind and class are just representative names to represent the actual structure of the object received by the API
export type APIDataType = { [key: string]: APIKindType };
@ -154,15 +154,15 @@ export type VertexBuildTypeAPI = {
data: VertexDataTypeAPI;
timestamp: string;
params: any;
messages: ChatOutputType[] | chatInputType[];
messages: ChatOutputType[] | ChatInputType[];
};
// data is the object received by the API
// it has results, artifacts, timedelta, duration
export type VertexDataTypeAPI = {
results: { [key: string]: string };
logs: any[];
messages: ChatOutputType[] | chatInputType[];
logs: { message: any; type: string }[];
message: ChatOutputType | ChatInputType;
inactive?: boolean;
timedelta?: number;
duration?: string;

View file

@ -22,15 +22,19 @@ export type ChatOutputType = {
files?: Array<{ path: string; type: string; name: string }>;
};
export type chatInputType = {
result: string;
export type ChatInputType = {
message: string;
sender: string;
sender_name: string;
stream_url?: string;
files?: Array<{ path: string; type: string; name: string }>;
};
export type FlowPoolObjectType = {
timestamp: string;
valid: boolean;
// list of chat outputs or list of chat inputs
messages: Array<ChatOutputType | chatInputType> | [];
data: { artifacts?: any; results: any | ChatOutputType | chatInputType };
messages: Array<ChatOutputType | ChatInputType> | [];
data: { artifacts: any; results: any | ChatOutputType | ChatInputType };
id: string;
};

View file

@ -487,7 +487,12 @@ export type headerFlowsType = {
style?: FlowStyleType;
};
export type chatInputType = {
export type ChatInputType = {
isDragging: boolean;
files: FilePreviewType[];
setFiles: (
files: FilePreviewType[] | ((prev: FilePreviewType[]) => FilePreviewType[])
) => void;
chatValue: string;
inputRef: {
current: any;
@ -526,6 +531,7 @@ export type fileCardPropsType = {
fileName: string;
content: string;
fileType: string;
showFile?: boolean;
};
export type nodeToolbarPropsType = {

View file

@ -9,28 +9,16 @@ import {
} from "reactflow";
import { BuildStatus } from "../../../constants/enums";
import { VertexBuildTypeAPI } from "../../api";
import { ChatInputType, ChatOutputType } from "../../chat";
import { FlowState } from "../../tabs";
export type chatInputType = {
result: string;
files?: string[];
};
export type ChatOutputType = {
message: string;
sender: string;
sender_name: string;
stream_url?: string;
files?: string[];
};
export type FlowPoolObjectType = {
timestamp: string;
valid: boolean;
messages: Array<ChatOutputType | chatInputType> | [];
messages: Array<ChatOutputType | ChatInputType> | [];
data: {
artifacts: any | ChatOutputType | chatInputType;
results: any | ChatOutputType | chatInputType;
artifacts: any | ChatOutputType | ChatInputType;
results: any | ChatOutputType | ChatInputType;
};
duration?: string;
progress?: number;
@ -45,8 +33,8 @@ export type FlowPoolObjectTypeNew = {
timestamp: string;
valid: boolean;
data: {
logs?: any | ChatOutputType | chatInputType;
results: any | ChatOutputType | chatInputType;
logs?: any | ChatOutputType | ChatInputType;
results: any | ChatOutputType | ChatInputType;
};
duration?: string;
progress?: number;
@ -98,7 +86,7 @@ export type FlowStoreType = {
state:
| FlowState
| undefined
| ((oldState: FlowState | undefined) => FlowState),
| ((oldState: FlowState | undefined) => FlowState)
) => void;
nodes: Node[];
edges: Edge[];
@ -106,11 +94,11 @@ export type FlowStoreType = {
onEdgesChange: OnEdgesChange;
setNodes: (
update: Node[] | ((oldState: Node[]) => Node[]),
skipSave?: boolean,
skipSave?: boolean
) => void;
setEdges: (
update: Edge[] | ((oldState: Edge[]) => Edge[]),
skipSave?: boolean,
skipSave?: boolean
) => void;
setNode: (id: string, update: Node | ((oldState: Node) => Node)) => void;
getNode: (id: string) => Node | undefined;
@ -118,12 +106,12 @@ export type FlowStoreType = {
deleteEdge: (edgeId: string | Array<string>) => void;
paste: (
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number },
position: { x: number; y: number; paneX?: number; paneY?: number }
) => void;
lastCopiedSelection: { nodes: any; edges: any } | null;
setLastCopiedSelection: (
newSelection: { nodes: any; edges: any } | null,
isCrop?: boolean,
isCrop?: boolean
) => void;
cleanFlow: () => void;
setFilterEdge: (newState) => void;
@ -150,7 +138,7 @@ export type FlowStoreType = {
verticesLayers: VertexLayerElementType[][];
runId: string;
verticesToRun: string[];
} | null,
} | null
) => void;
addToVerticesBuild: (vertices: string[]) => void;
removeFromVerticesBuild: (vertices: string[]) => void;
@ -167,8 +155,8 @@ export type FlowStoreType = {
};
updateFlowPool: (
nodeId: string,
data: VertexBuildTypeAPI | ChatOutputType | chatInputType,
buildId?: string,
data: VertexBuildTypeAPI | ChatOutputType | ChatInputType,
buildId?: string
) => void;
getNodePosition: (nodeId: string) => { x: number; y: number };
};

View file

@ -1,4 +1,4 @@
export type chatInputType = {
export type ChatInputType = {
result: string;
};
@ -12,7 +12,7 @@ export type FlowPoolObjectType = {
timestamp: string;
valid: boolean;
params: any;
data: { artifacts: any; results: any | ChatOutputType | chatInputType };
data: { artifacts: any; results: any | ChatOutputType | ChatInputType };
id: string;
};

View file

@ -93,6 +93,7 @@ import {
Package2,
Palette,
Paperclip,
PaperclipIcon,
Pencil,
PencilLine,
Pin,
@ -533,6 +534,7 @@ export const nodeIconsLucide: iconsType = {
FolderPlusIcon,
FolderIcon,
Discord: FaDiscord,
PaperclipIcon,
RotateCcw,
Settings,
Streamlit,