Release 0.0.75 (#329)

🚀  Release 0.0.75
We are excited to announce the latest updates and features that have been added to Langflow! Here’s a summary of the recent changes:

New Features:
- Deploy Langflow using Langchain-Serve: Thanks to the hard work of @deepankarm, Langflow can now be deployed using Langchain-Serve to Jina AI Cloud. This integration brings enhanced functionality and improves the overall performance of Langflow. Just run `langflow --jcloud`.
- Copy and Paste Nodes and Flows: @anovazzi1 and @lucaseduoli have implemented the copy and paste functionality in Langflow. Now you can easily duplicate nodes and flows, improving productivity and workflow efficiency.
- Undo/Redo Functionality: @anovazzi1 and @lucaseduoli have also added an undo/redo feature to LangFlow, allowing you to easily revert and redo changes made to nodes and flows. This enhances the editing experience and provides greater control over your work.

Improvements and Fixes:
- Build: The latest update includes making Langchain-Serve optional, allowing for more flexibility in deployment.
- Documentation: The README has been updated to provide information about JCloud deployment.
- Validation Fixes: @ogabrielluiz has made several updates to improve validation in Langflow, including returning exception messages in responses and adding validation status icons and tooltips to node properties.
- Bug Fixes: Several bug fixes have been implemented, including resizing issues in the chat input, copy and paste bugs in the code area, and padding adjustments in the UI. Special thanks to @anovazzi1 and @lucaseduoli for their contributions in fixing these issues.

We appreciate the contributions from the following members:
- @deepankarm
- @anovazzi1
- @lucaseduoli

Thank you to everyone who has contributed to this release. Your efforts have made Langflow even better! If you have any feedback or suggestions, please don’t hesitate to reach out. Happy coding with Langflow! 🎉
This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-05-23 11:25:54 -03:00 • committed by GitHub
commit 9d910b769a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2418 additions and 1018 deletions

View file

@ -47,3 +47,11 @@ jobs:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }} POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
run: | run: |
poetry publish poetry publish
- name: Trigger build and push on langchain-serve
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.SERVE_GITHUB_TOKEN }}
repository: jina-ai/langchain-serve
event-type: langflow-push
client-payload: '{"push_token": "${{ secrets.LCSERVE_PUSH_TOKEN }}", "branch": "dev"}'

View file

@ -46,6 +46,17 @@ build:
poetry build --format sdist poetry build --format sdist
rm -rf src/backend/langflow/frontend rm -rf src/backend/langflow/frontend
lcserve_push:
make build_frontend
@version=$$(poetry version --short); \
lc-serve push --app langflow.lcserve:app --app-dir . \
--image-name langflow --image-tag $${version} --verbose
lcserve_deploy:
@:$(if $(uses),,$(error `uses` is not set. Please run `make uses=... lcserve_deploy`))
lc-serve deploy jcloud --app langflow.lcserve:app --app-dir . \
--uses $(uses) --config src/backend/langflow/jcloud.yml --verbose
dev: dev:
make install_frontend make install_frontend
ifeq ($(build),1) ifeq ($(build),1)

View file

@ -46,13 +46,98 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/logspace-ai/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md) [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/logspace-ai/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md)
### Deploy Langflow on Google Cloud Platform ### Deploy Langflow on [Jina AI Cloud](https://github.com/jina-ai/langchain-serve)
Follow our step-by-step guide to deploy Langflow on Google Cloud Platform (GCP) using Google Cloud Shell. The guide is available in the [**Langflow in Google Cloud Platform**](GCP_DEPLOYMENT.md) document. Langflow integrates with langchain-serve to provide a one-command deployment to Jina AI Cloud.
Alternatively, click the **"Open in Cloud Shell"** button below to launch Google Cloud Shell, clone the Langflow repository, and start an **interactive tutorial** that will guide you through the process of setting up the necessary resources and deploying Langflow on your GCP project. Start by installing `langchain-serve` with
[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/genome21/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md) ```bash
pip install -U langchain-serve
```
Then, run:
```bash
langflow --jcloud
```
```text
🎉 Langflow server successfully deployed on Jina AI Cloud 🎉
🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): https://<your-app>.wolf.jina.ai/
📖 Read more about managing the server: https://github.com/jina-ai/langchain-serve
```
<details>
<summary>Show complete (example) output</summary>
```text
🚀 Deploying Langflow server on Jina AI Cloud
╭───────────────────────── 🎉 Flow is available! ──────────────────────────╮
│ │
│ ID langflow-e3dd8820ec │
│ Gateway (Websocket) wss://langflow-e3dd8820ec.wolf.jina.ai │
│ Dashboard https://dashboard.wolf.jina.ai/flow/e3dd8820ec │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
╭──────────────┬──────────────────────────────────────────────────────────────────────────────╮
│ App ID │ langflow-e3dd8820ec │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ Phase │ Serving │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ Endpoint │ wss://langflow-e3dd8820ec.wolf.jina.ai │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ App logs │ dashboards.wolf.jina.ai │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ Swagger UI │ https://langflow-e3dd8820ec.wolf.jina.ai/docs │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ OpenAPI JSON │ https://langflow-e3dd8820ec.wolf.jina.ai/openapi.json │
╰──────────────┴──────────────────────────────────────────────────────────────────────────────╯
🎉 Langflow server successfully deployed on Jina AI Cloud 🎉
🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): https://langflow-e3dd8820ec.wolf.jina.ai/
📖 Read more about managing the server: https://github.com/jina-ai/langchain-serve
```
</details>
#### API Usage
You can use Langflow directly on your browser, or use the API endpoints on Jina AI Cloud to interact with the server.
<details>
<summary>Show API usage (with python)</summary>
```python
import json
import requests
FLOW_PATH = "Time_traveller.json"
# HOST = 'http://localhost:7860'
HOST = 'https://langflow-f1ed20e309.wolf.jina.ai'
API_URL = f'{HOST}/predict'
def predict(message):
with open(FLOW_PATH, "r") as f:
json_data = json.load(f)
payload = {'exported_flow': json_data, 'message': message}
response = requests.post(API_URL, json=payload)
return response.json()
predict('Take me to 1920s Bangalore')
```
```json
{
"result": "Great choice! Bangalore in the 1920s was a vibrant city with a rich cultural and political scene. Here are some suggestions for things to see and do:\n\n1. Visit the Bangalore Palace - built in 1887, this stunning palace is a perfect example of Tudor-style architecture. It was home to the Maharaja of Mysore and is now open to the public.\n\n2. Attend a performance at the Ravindra Kalakshetra - this cultural center was built in the 1920s and is still a popular venue for music and dance performances.\n\n3. Explore the neighborhoods of Basavanagudi and Malleswaram - both of these areas have retained much of their old-world charm and are great places to walk around and soak up the atmosphere.\n\n4. Check out the Bangalore Club - founded in 1868, this exclusive social club was a favorite haunt of the British expat community in the 1920s.\n\n5. Attend a meeting of the Indian National Congress - founded in 1885, the INC was a major force in the Indian independence movement and held many meetings and rallies in Bangalore in the 1920s.\n\nHope you enjoy your trip to 1920s Bangalore!"
}
```
</details>
> Read more about resource customization, cost, and management of Langflow apps on Jina AI Cloud in the **[langchain-serve](https://github.com/jina-ai/langchain-serve)** repository.
## 🎨 Creating Flows ## 🎨 Creating Flows

16
lcserve.Dockerfile Normal file
View file

@ -0,0 +1,16 @@
# This file is used by `lc-serve` to build the image.
# Don't change the name of this file.
FROM jinawolf/serving-gateway:${version}
RUN apt-get update \
&& apt-get install --no-install-recommends -y build-essential libpq-dev
COPY . /appdir/
RUN pip install poetry==1.4.0 && cd /appdir && pip install . && \
pip uninstall -y poetry && \
apt-get remove --auto-remove -y build-essential libpq-dev && \
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/*
ENTRYPOINT [ "jina", "gateway", "--uses", "config.yml" ]

1229
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "0.0.74" version = "0.0.75"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"] authors = ["Logspace <contact@logspace.ai>"]
maintainers = [ maintainers = [
@ -29,7 +29,7 @@ google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0" google-api-python-client = "^2.79.0"
typer = "^0.7.0" typer = "^0.7.0"
gunicorn = "^20.1.0" gunicorn = "^20.1.0"
langchain = "^0.0.170" langchain = "^0.0.176"
openai = "^0.27.2" openai = "^0.27.2"
types-pyyaml = "^6.0.12.8" types-pyyaml = "^6.0.12.8"
dill = "^0.3.6" dill = "^0.3.6"
@ -51,6 +51,7 @@ websockets = "^11.0.2"
tiktoken = "^0.3.3" tiktoken = "^0.3.3"
wikipedia = "^1.4.0" wikipedia = "^1.4.0"
gptcache = "^0.1.23" gptcache = "^0.1.23"
langchain-serve = { version = "^0.0.33", optional = true }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^23.1.0" black = "^23.1.0"
@ -66,6 +67,9 @@ pandas-stubs = "^2.0.0.230412"
types-pillow = "^9.5.0.2" types-pillow = "^9.5.0.2"
[tool.poetry.extras]
deploy = ["langchain-serve"]
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120

View file

@ -33,11 +33,15 @@ def serve(
config: str = typer.Option("config.yaml", help="Path to the configuration file."), config: str = typer.Option("config.yaml", help="Path to the configuration file."),
log_level: str = typer.Option("info", help="Logging level."), log_level: str = typer.Option("info", help="Logging level."),
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."), log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
): ):
""" """
Run the Langflow server. Run the Langflow server.
""" """
if jcloud:
return serve_on_jcloud()
configure(log_level=log_level, log_file=log_file) configure(log_level=log_level, log_file=log_file)
update_settings(config) update_settings(config)
app = create_app() app = create_app()
@ -69,6 +73,52 @@ def serve(
LangflowApplication(app, options).run() LangflowApplication(app, options).run()
def serve_on_jcloud():
"""
Deploy Langflow server on Jina AI Cloud
"""
import asyncio
from importlib.metadata import version as mod_version
import click
try:
from lcserve.__main__ import serve_on_jcloud # type: ignore
except ImportError:
click.secho(
"🚨 Please install langchain-serve to deploy Langflow server on Jina AI Cloud "
"using `pip install langchain-serve`",
fg="red",
)
return
app_name = "langflow.lcserve:app"
app_dir = str(Path(__file__).parent)
version = mod_version("langflow")
base_image = "jinaai+docker://deepankarm/langflow"
click.echo("🚀 Deploying Langflow server on Jina AI Cloud")
app_id = asyncio.run(
serve_on_jcloud(
fastapi_app_str=app_name,
app_dir=app_dir,
uses=f"{base_image}:{version}",
name="langflow",
)
)
click.secho(
"🎉 Langflow server successfully deployed on Jina AI Cloud 🎉", fg="green"
)
click.secho(
"🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): ",
nl=False,
fg="green",
)
click.secho(f"https://{app_id}.wolf.jina.ai/", fg="blue")
click.secho("📖 Read more about managing the server: ", nl=False, fg="green")
click.secho("https://github.com/jina-ai/langchain-serve", fg="blue")
def main(): def main():
app() app()

View file

@ -1,5 +1,5 @@
from importlib.metadata import version
import logging import logging
from importlib.metadata import version
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException

View file

@ -54,4 +54,4 @@ def post_validate_node(node_id: str, data: dict):
return json.dumps({"valid": True, "params": str(node._built_object_repr())}) return json.dumps({"valid": True, "params": str(node._built_object_repr())})
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
return json.dumps({"valid": False}) return json.dumps({"valid": False, "params": str(e)})

View file

@ -143,7 +143,8 @@ class DocumentLoaderNode(Node):
# This built_object is a list of documents. Maybe we should # This built_object is a list of documents. Maybe we should
# show how many documents are in the list? # show how many documents are in the list?
if self._built_object: if self._built_object:
return f"""{self.node_type}({len(self._built_object)} documents)\nDocuments: {self._built_object[:3]}...""" return f"""{self.node_type}({len(self._built_object)} documents)
Documents: {self._built_object[:3]}..."""
return f"{self.node_type}()" return f"{self.node_type}()"

View file

@ -1,8 +1,10 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import embedding_type_to_cls_dict from langflow.interface.custom_lists import embedding_type_to_cls_dict
from langflow.settings import settings from langflow.settings import settings
from langflow.template.base import FrontendNode
from langflow.template.nodes import EmbeddingFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -14,6 +16,10 @@ class EmbeddingCreator(LangChainTypeCreator):
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
return embedding_type_to_cls_dict return embedding_type_to_cls_dict
@property
def frontend_node_class(self) -> Type[FrontendNode]:
return EmbeddingFrontendNode
def get_signature(self, name: str) -> Optional[Dict]: def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of an embedding.""" """Get the signature of an embedding."""
try: try:

View file

@ -108,6 +108,7 @@ def instantiate_toolkit(node_type, class_object, params):
def instantiate_embedding(class_object, params): def instantiate_embedding(class_object, params):
params.pop("model", None) params.pop("model", None)
params.pop("headers", None)
try: try:
return class_object(**params) return class_object(**params)
except ValidationError: except ValidationError:

View file

@ -3,12 +3,12 @@ import io
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from chromadb.errors import NotEnoughElementsException # type: ignore from chromadb.errors import NotEnoughElementsException # type: ignore
from langchain.schema import AgentAction
from langflow.api.callback import AsyncStreamingLLMCallbackHandler, StreamingLLMCallbackHandler # type: ignore from langflow.api.callback import AsyncStreamingLLMCallbackHandler, StreamingLLMCallbackHandler # type: ignore
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict
from langflow.graph.graph import Graph from langflow.graph.graph import Graph
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langchain.schema import AgentAction
def load_langchain_object(data_graph, is_first_message=False): def load_langchain_object(data_graph, is_first_message=False):

View file

@ -0,0 +1,2 @@
instance: C4
autoscale_min: 1

View file

@ -0,0 +1,16 @@
# This file is used by lc-serve to load the mounted app and serve it.
from pathlib import Path
from fastapi.staticfiles import StaticFiles
from langflow.main import create_app
app = create_app()
path = Path(__file__).parent
static_files_dir = path / "frontend"
app.mount(
"/",
StaticFiles(directory=static_files_dir, html=True),
name="static",
)

View file

@ -122,6 +122,7 @@ class MidJourneyPromptChainNode(FrontendNode):
advanced=False, advanced=False,
multiline=False, multiline=False,
name="llm", name="llm",
display_name="LLM",
), ),
TemplateField( TemplateField(
field_type="BaseChatMemory", field_type="BaseChatMemory",
@ -156,6 +157,7 @@ class TimeTravelGuideChainNode(FrontendNode):
advanced=False, advanced=False,
multiline=False, multiline=False,
name="llm", name="llm",
display_name="LLM",
), ),
TemplateField( TemplateField(
field_type="BaseChatMemory", field_type="BaseChatMemory",
@ -210,6 +212,7 @@ class SeriesCharacterChainNode(FrontendNode):
advanced=False, advanced=False,
multiline=False, multiline=False,
name="llm", name="llm",
display_name="LLM",
), ),
], ],
) )
@ -295,6 +298,7 @@ class JsonAgentNode(FrontendNode):
required=True, required=True,
show=True, show=True,
name="llm", name="llm",
display_name="LLM",
), ),
], ],
) )
@ -341,6 +345,7 @@ class InitializeAgentNode(FrontendNode):
required=True, required=True,
show=True, show=True,
name="llm", name="llm",
display_name="LLM",
advanced=False, advanced=False,
), ),
], ],
@ -376,6 +381,7 @@ class CSVAgentNode(FrontendNode):
required=True, required=True,
show=True, show=True,
name="llm", name="llm",
display_name="LLM",
), ),
], ],
) )
@ -614,3 +620,11 @@ class LLMFrontendNode(FrontendNode):
elif field.name in ["model_name", "temperature"]: elif field.name in ["model_name", "temperature"]:
field.advanced = False field.advanced = False
field.show = True field.show = True
class EmbeddingFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if field.name == "headers":
field.show = False

View file

@ -16,7 +16,6 @@ import CrashErrorComponent from "./components/CrashErrorComponent";
import { TabsContext } from "./contexts/tabsContext"; import { TabsContext } from "./contexts/tabsContext";
export default function App() { export default function App() {
let { setCurrent, setShowSideBar, setIsStackedOpen } = let { setCurrent, setShowSideBar, setIsStackedOpen } =
useContext(locationContext); useContext(locationContext);
let location = useLocation(); let location = useLocation();
@ -47,6 +46,15 @@ export default function App() {
}> }>
>([]); >([]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
fetch("/version")
.then((res) => res.json())
.then((data) => {
setVersion(data.version);
});
}, []);
// Use effect hook to update alertsList when a new alert is added // Use effect hook to update alertsList when a new alert is added
useEffect(() => { useEffect(() => {
// If there is an error alert open with data, add it to the alertsList // If there is an error alert open with data, add it to the alertsList
@ -159,9 +167,10 @@ export default function App() {
<a <a
target={"_blank"} target={"_blank"}
href="https://logspace.ai/" href="https://logspace.ai/"
className="absolute bottom-2 left-6 text-gray-300 px-2 rounded-lg text-xs cursor-pointer font-sans tracking-wide bg-gray-800 dark:bg-gray-300 dark:text-gray-800 " className="absolute left-7 bottom-2 flex h-6 cursor-pointer flex-col items-center justify-start overflow-hidden rounded-lg bg-gray-800 px-2 text-center font-sans text-xs tracking-wide text-gray-300 transition-all duration-500 ease-in-out hover:h-12 dark:bg-gray-100 dark:text-gray-800"
> >
Created by Logspace {version && <div className="mt-1">⛓️ LangFlow v{version}</div>}
<div className="mt-2">Created by Logspace</div>
</a> </a>
</div> </div>
); );

View file

@ -1,4 +1,12 @@
import { BugAntIcon, Cog6ToothIcon, ExclamationCircleIcon, InformationCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; import {
BugAntIcon,
CheckCircleIcon,
Cog6ToothIcon,
EllipsisHorizontalCircleIcon,
ExclamationCircleIcon,
InformationCircleIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { classNames, nodeColors, nodeIcons, toNormalCase } from "../../utils"; import { classNames, nodeColors, nodeIcons, toNormalCase } from "../../utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
@ -51,11 +59,7 @@ export default function GenericNode({
let jsonResponse = await response.json(); let jsonResponse = await response.json();
let jsonResponseParsed = await JSON.parse(jsonResponse); let jsonResponseParsed = await JSON.parse(jsonResponse);
console.log(jsonResponseParsed); console.log(jsonResponseParsed);
if(jsonResponseParsed.valid){ setValidationStatus(jsonResponseParsed);
setValidationStatus(jsonResponseParsed.params);
} else {
setValidationStatus("error");
}
} }
} catch (error) { } catch (error) {
// console.error("Error validating node:", error); // console.error("Error validating node:", error);
@ -70,14 +74,6 @@ export default function GenericNode({
} }
}, [params, validateNode]); }, [params, validateNode]);
useEffect(() => {
if (validationStatus !== "error") {
setIsValid(true);
} else {
setIsValid(false);
}
}, [validationStatus]);
if (!Icon) { if (!Icon) {
if (showError.current) { if (showError.current) {
setErrorData({ setErrorData({
@ -94,7 +90,6 @@ export default function GenericNode({
return ( return (
<div <div
className={classNames( className={classNames(
isValid ? "animate-pulse-green" : "border-red-outline",
selected ? "border border-blue-500" : "border dark:border-gray-700", selected ? "border border-blue-500" : "border dark:border-gray-700",
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center" "prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center"
)} )}
@ -108,15 +103,48 @@ export default function GenericNode({
}} }}
/> />
<div className="ml-2 truncate">{data.type}</div> <div className="ml-2 truncate">{data.type}</div>
{validationStatus && validationStatus !== "error" ? <div>
<Tooltip title={ <Tooltip
title={
!validationStatus ? (
"Validating..."
) : (
<div className="max-h-96 overflow-auto"> <div className="max-h-96 overflow-auto">
{validationStatus} {validationStatus.params.split("\n").map((line, index) => (
</div>}> <div key={index}>{line}</div>
<ExclamationCircleIcon className="w-5 hover:text-gray-500 hover:dark:text-gray-300" /> ))}
</div>
)
}
>
<div className="relative w-5 h-5">
<CheckCircleIcon
className={classNames(
validationStatus && validationStatus.valid
? "text-green-500 opacity-100"
: "text-green-500 opacity-0 animate-spin",
"absolute w-5 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
)}
/>
<ExclamationCircleIcon
className={classNames(
validationStatus && !validationStatus.valid
? "text-red-500 opacity-100"
: "text-red-500 opacity-0 animate-spin",
"w-5 absolute hover:text-gray-500 hover:dark:text-gray-600 transition-all ease-in-out duration-200"
)}
/>
<EllipsisHorizontalCircleIcon
className={classNames(
!validationStatus
? "text-yellow-500 opacity-100"
: "text-yellow-500 opacity-0 animate-spin",
"w-5 absolute hover:text-gray-500 hover:dark:text-gray-600 transition-all ease-in-out duration-300"
)}
/>
</div>
</Tooltip> </Tooltip>
: <></> </div>
}
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button

View file

@ -21,10 +21,10 @@ export default function ChatTrigger({ open, setOpen }) {
leaveFrom="translate-y-0" leaveFrom="translate-y-0"
leaveTo="translate-y-96" leaveTo="translate-y-96"
> >
<div className="absolute bottom-6 right-3"> <div className="absolute bottom-4 right-3">
<div <div
style={{ backgroundColor: nodeColors["chat"] }} // style={{ backgroundColor: nodeColors["chat"] }}
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full dark:bg-gray-800 dark:border-gray-600 dark:text-white" className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:border-gray-600"
> >
<button <button
onClick={() => { onClick={() => {

View file

@ -14,7 +14,7 @@ export default function FloatComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
const {setDisableCP} = useContext(TabsContext) const {setDisableCopyPaste} = useContext(TabsContext)
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<input <input
@ -30,10 +30,10 @@ export default function FloatComponent({
onChange(e.target.value); onChange(e.target.value);
}} }}
onBlur={() => { onBlur={() => {
setDisableCP(false) setDisableCopyPaste(false)
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true) setDisableCopyPaste(true)
}} }}
/> />
</div> </div>

View file

@ -11,7 +11,7 @@ export default function InputComponent({
}: InputComponentType) { }: InputComponentType) {
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const {setDisableCP} = useContext(TabsContext) const {setDisableCopyPaste} = useContext(TabsContext)
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
@ -29,10 +29,10 @@ export default function InputComponent({
<input <input
value={myValue} value={myValue}
onBlur={() => { onBlur={() => {
setDisableCP(false) setDisableCopyPaste(false)
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true) setDisableCopyPaste(true)
}} }}
className={classNames( className={classNames(
"block w-full pr-12 form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", "block w-full pr-12 form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",

View file

@ -17,7 +17,7 @@ export default function InputListComponent({
onChange([""]); onChange([""]);
} }
}, [disabled, onChange]); }, [disabled, onChange]);
const {setDisableCP} = useContext(TabsContext) const {setDisableCopyPaste} = useContext(TabsContext)
return ( return (
<div <div
className={ className={
@ -44,10 +44,10 @@ export default function InputListComponent({
onChange(inputList); onChange(inputList);
}} }}
onBlur={() => { onBlur={() => {
setDisableCP(false) setDisableCopyPaste(false)
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true) setDisableCopyPaste(true)
}} }}
/> />
{idx === inputList.length - 1 ? ( {idx === inputList.length - 1 ? (

View file

@ -14,7 +14,7 @@ export default function IntComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
const {setDisableCP} =useContext(TabsContext) const {setDisableCopyPaste} =useContext(TabsContext)
return ( return (
<div <div
className={ className={
@ -46,10 +46,10 @@ export default function IntComponent({
onChange(e.target.value); onChange(e.target.value);
}} }}
onBlur={() => { onBlur={() => {
setDisableCP(false) setDisableCopyPaste(false)
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true) setDisableCopyPaste(true)
}} }}
/> />

View file

@ -21,8 +21,8 @@ export default function ToggleComponent({
setEnabled(x); setEnabled(x);
}} }}
className={classNames( className={classNames(
enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-600", enabled ? 'bg-indigo-600' : 'bg-gray-200',
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out " 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2'
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>
@ -44,19 +44,6 @@ export default function ToggleComponent({
)} )}
aria-hidden="true" aria-hidden="true"
> >
<svg
className="h-3 w-3 text-gray-400"
fill="none"
viewBox="0 0 12 12"
>
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span> </span>
<span <span
className={classNames( className={classNames(
@ -67,13 +54,6 @@ export default function ToggleComponent({
)} )}
aria-hidden="true" aria-hidden="true"
> >
<svg
className="h-3 w-3 text-indigo-600"
fill="currentColor"
viewBox="0 0 12 12"
>
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
</svg>
</span> </span>
</span> </span>
</Switch> </Switch>

View file

@ -6,13 +6,14 @@ import {
ReactNode, ReactNode,
useContext, useContext,
} from "react"; } from "react";
import { FlowType } from "../types/flow"; import { FlowType, NodeType } from "../types/flow";
import { LangFlowState, TabsContextType } from "../types/tabs"; import { LangFlowState, TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils"; import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils";
import { alertContext } from "./alertContext"; import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext"; import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api"; import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { addEdge } from "reactflow";
const TabsContextInitialValue: TabsContextType = { const TabsContextInitialValue: TabsContextType = {
save: () => {}, save: () => {},
@ -22,12 +23,14 @@ const TabsContextInitialValue: TabsContextType = {
removeFlow: (id: string) => {}, removeFlow: (id: string) => {},
addFlow: (flowData?: any) => {}, addFlow: (flowData?: any) => {},
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => 0, incrementNodeId: () => uuidv4(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {}, uploadFlow: () => {},
hardReset: () => {}, hardReset: () => {},
disableCP:false, disableCopyPaste:false,
setDisableCP:(state:boolean)=>{}, setDisableCopyPaste:(state:boolean)=>{},
getNodeId: () => "",
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => {},
}; };
export const TabsContext = createContext<TabsContextType>( export const TabsContext = createContext<TabsContextType>(
@ -39,24 +42,24 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
const [flows, setFlows] = useState<Array<FlowType>>([]); const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uuidv4()); const [id, setId] = useState(uuidv4());
const { templates } = useContext(typesContext); const { templates, reactFlowInstance } = useContext(typesContext);
const newNodeId = useRef(0); const newNodeId = useRef(uuidv4());
function incrementNodeId() { function incrementNodeId() {
newNodeId.current = newNodeId.current + 1; newNodeId.current = uuidv4();
return newNodeId.current; return newNodeId.current;
} }
function save() { function save() {
if (flows.length !== 0) if (flows.length !== 0)
window.localStorage.setItem( window.localStorage.setItem(
"tabsData", "tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current }) JSON.stringify({ tabIndex, flows, id})
); );
} }
useEffect(() => { useEffect(() => {
//save tabs locally //save tabs locally
// console.log(id)
save(); save();
}, [flows, id, tabIndex, newNodeId]); }, [flows, id, tabIndex, newNodeId]);
useEffect(() => { useEffect(() => {
@ -80,12 +83,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
setTabIndex(cookieObject.tabIndex); setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows); setFlows(cookieObject.flows);
setId(cookieObject.id); setId(cookieObject.id);
newNodeId.current = cookieObject.nodeId;
} }
}, [templates]); }, [templates]);
function hardReset() { function hardReset() {
newNodeId.current = 0; newNodeId.current = uuidv4();
setTabIndex(0); setTabIndex(0);
setFlows([]); setFlows([]);
setId(uuidv4()); setId(uuidv4());
@ -112,6 +114,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
} }
function getNodeId() {
return `dndnode_` + incrementNodeId();
}
/** /**
* Creates a file input and listens to a change event to upload a JSON flow file. * Creates a file input and listens to a change event to upload a JSON flow file.
* If the file type is application/json, the file is read and parsed into a JSON object. * If the file type is application/json, the file is read and parsed into a JSON object.
@ -165,6 +171,90 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Add a new flow to the list of flows. * Add a new flow to the list of flows.
* @param flow Optional flow to add. * @param flow Optional flow to add.
*/ */
function paste(selectionInstance, position){
console.log(position);
console.log(selectionInstance)
let minimumX = Infinity;
let minimumY = Infinity;
let idsMap = {};
let nodes = reactFlowInstance.getNodes();
let edges = reactFlowInstance.getEdges();
selectionInstance.nodes.forEach((n) => {
if (n.position.y < minimumY) {
minimumY = n.position.y;
}
if (n.position.x < minimumX) {
minimumX = n.position.x;
}
});
const insidePosition = reactFlowInstance.project(position);
selectionInstance.nodes.forEach((n) => {
// Generate a unique node ID
let newId = getNodeId();
idsMap[n.id] = newId;
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
position: {
x: insidePosition.x + n.position.x - minimumX,
y: insidePosition.y + n.position.y - minimumY,
},
data: {
...n.data,
id: newId,
},
};
// Add the new node to the list of nodes in state
nodes = nodes
.map((e) => ({ ...e, selected: false }))
.concat({ ...newNode, selected: false })
console.log(nodes);
});
reactFlowInstance.setNodes(nodes);
selectionInstance.edges.forEach((e) => {
let source = idsMap[e.source];
let target = idsMap[e.target];
let sourceHandleSplitted = e.sourceHandle.split("|");
let sourceHandle =
sourceHandleSplitted[0] +
"|" +
source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = e.targetHandle.split("|");
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
let id =
"reactflow__edge-" +
source +
sourceHandle +
"-" +
target +
targetHandle;
edges = addEdge(
{
source,
target,
sourceHandle,
targetHandle,
id,
className: "animate-pulse",
selected: false,
},
edges.map((e) => ({ ...e, selected: false }))
);
console.log(edges);
});
reactFlowInstance.setEdges(edges);
};
function addFlow(flow?: FlowType) { function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided. // Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null; const data = flow?.data ? flow.data : null;
@ -192,6 +282,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
setId(uuidv4()); setId(uuidv4());
// Add the new flow to the list of flows. // Add the new flow to the list of flows.
setFlows((prevState) => { setFlows((prevState) => {
const newFlows = [...prevState, newFlow]; const newFlows = [...prevState, newFlow];
return newFlows; return newFlows;
@ -216,13 +307,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return newFlows; return newFlows;
}); });
} }
const [disableCP, setDisableCP] = useState(false); const [disableCopyPaste, setDisableCopyPaste] = useState(false);
return ( return (
<TabsContext.Provider <TabsContext.Provider
value={{ value={{
disableCP, disableCopyPaste,
setDisableCP, setDisableCopyPaste,
save, save,
hardReset, hardReset,
tabIndex, tabIndex,
@ -234,6 +325,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
updateFlow, updateFlow,
downloadFlow, downloadFlow,
uploadFlow, uploadFlow,
getNodeId,
paste,
}} }}
> >
{children} {children}

View file

@ -27,11 +27,19 @@ export function TypesProvider({ children }: { children: ReactNode }) {
const [data, setData] = useState({}); const [data, setData] = useState({});
useEffect(() => { useEffect(() => {
async function getTypes(): Promise<void> { let delay = 1000; // Start delay of 1 second
// Make an asynchronous API call to retrieve all data. let intervalId = null;
let result = await getAll(); let retryCount = 0; // Count of retry attempts
const maxRetryCount = 5; // Max retry attempts
// Update the state of the component with the retrieved data. // We will keep a flag to handle the case where the component is unmounted before the API call resolves.
let isMounted = true;
async function getTypes(): Promise<void> {
try {
const result = await getAll();
// Make sure to only update the state if the component is still mounted.
if (isMounted) {
setData(result.data); setData(result.data);
setTemplates( setTemplates(
Object.keys(result.data).reduce((acc, curr) => { Object.keys(result.data).reduce((acc, curr) => {
@ -55,9 +63,34 @@ export function TypesProvider({ children }: { children: ReactNode }) {
}, {}) }, {})
); );
} }
// Call the getTypes function. // Clear the interval if successful.
getTypes(); clearInterval(intervalId);
}, [setTypes]); } catch (error) {
retryCount++;
// On error, double the delay for the next attempt up to a maximum.
delay = Math.min(30000, delay * 2);
// Log errors but don't do anything else - the function will try again on the next interval.
console.error(error);
// Clear the old interval and start a new one with the new delay.
if (retryCount <= maxRetryCount) {
clearInterval(intervalId);
intervalId = setInterval(getTypes, delay);
} else {
console.error("Max retry attempts reached. Stopping retries.");
}
}
}
// Start the initial interval.
intervalId = setInterval(getTypes, delay);
return () => {
// This will clear the interval when the component unmounts, or when the dependencies of the useEffect hook change.
clearInterval(intervalId);
// Indicate that the component has been unmounted.
isMounted = false;
};
}, []);
function deleteNode(idx: string) { function deleteNode(idx: string) {
reactFlowInstance.setNodes( reactFlowInstance.setNodes(

View file

@ -1,41 +1,64 @@
import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline"; import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import { useRef } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import { TabsContext } from "../../../contexts/tabsContext";
export default function ChatInput({ export default function ChatInput({
lockChat, lockChat,
chatValue, chatValue,
sendMessage, sendMessage,
setChatValue, setChatValue,
}: { inputRef,
lockChat: boolean;
chatValue: string;
sendMessage: Function;
setChatValue: Function;
}) { }) {
const inputRef = useRef(null); useEffect(() => {
if (inputRef.current) {
inputRef.current.style.height = "inherit"; // Reset the height
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
}
}, [chatValue]);
const { setDisableCopyPaste } = useContext(TabsContext);
return ( return (
<> <div className="relative">
<textarea <textarea
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) { if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage(); sendMessage();
} }
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
rows={1}
ref={inputRef} ref={inputRef}
disabled={lockChat} disabled={lockChat}
style={{ resize: "none" }} style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : chatValue} value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => { onChange={(e) => {
setChatValue(e.target.value); setChatValue(e.target.value);
}} }}
className={classNames( className={classNames(
lockChat ? "bg-gray-300 text-black dark:bg-gray-700 dark:text-gray-300" : "bg-gray-200 text-black dark:bg-gray-900 dark:text-gray-300", lockChat
"form-input block w-full custom-scroll h-10 rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" ? " bg-gray-300 text-black dark:bg-gray-700 dark:text-gray-300"
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm"
)} )}
placeholder={"Send a message..."} placeholder={"Send a message..."}
/> />
<div className="absolute inset-y-0 right-0 flex items-center pr-3"> <div className="absolute bottom-0.5 right-3">
<button disabled={lockChat} onClick={() => sendMessage()}> <button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? ( {lockChat ? (
<LockClosedIcon <LockClosedIcon
@ -50,6 +73,6 @@ export default function ChatInput({
)} )}
</button> </button>
</div> </div>
</> </div>
); );
} }

View file

@ -1,5 +1,8 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { ChatBubbleOvalLeftEllipsisIcon, XMarkIcon } from "@heroicons/react/24/outline"; import {
ChatBubbleOvalLeftEllipsisIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useEffect, useRef, useState } from "react"; import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { FlowType, NodeType } from "../../types/flow"; import { FlowType, NodeType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
@ -37,8 +40,7 @@ export default function ChatModal({
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
id.current = flow.id; id.current = flow.id;
},[flow.id]) }, [flow.id]);
var isStream = false; var isStream = false;
@ -144,6 +146,9 @@ export default function ChatModal({
isStream = true; isStream = true;
} }
if (data.type === "end") { if (data.type === "end") {
if (data.message) {
updateLastMessage({ str: data.message, end: true });
}
if (data.intermediate_steps) { if (data.intermediate_steps) {
updateLastMessage({ updateLastMessage({
str: data.message, str: data.message,
@ -171,7 +176,8 @@ export default function ChatModal({
const urlWs = const urlWs =
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? `ws://localhost:7860/chat/${id.current}` ? `ws://localhost:7860/chat/${id.current}`
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host : `${window.location.protocol === "https:" ? "wss" : "ws"}://${
window.location.host
}/chat/${id.current}`; }/chat/${id.current}`;
const newWs = new WebSocket(urlWs); const newWs = new WebSocket(urlWs);
newWs.onopen = () => { newWs.onopen = () => {
@ -189,10 +195,9 @@ export default function ChatModal({
}; };
newWs.onerror = (ev) => { newWs.onerror = (ev) => {
console.log(ev, "error"); console.log(ev, "error");
if(flow.id===""){ if (flow.id === "") {
connectWS(); connectWS();
} } else {
else{
setErrorData({ setErrorData({
title: "There was an error on web connection, please: ", title: "There was an error on web connection, please: ",
list: [ list: [
@ -205,10 +210,9 @@ export default function ChatModal({
}; };
ws.current = newWs; ws.current = newWs;
} catch { } catch {
if(flow.id===""){ if (flow.id === "") {
connectWS(); connectWS();
} } else {
else{
setErrorData({ setErrorData({
title: "There was an error on web connection, please: ", title: "There was an error on web connection, please: ",
list: [ list: [
@ -279,7 +283,8 @@ export default function ChatModal({
e.targetHandle.split("|")[2] === n.id e.targetHandle.split("|")[2] === n.id
) )
? [ ? [
`${type} is missing ${template.display_name `${type} is missing ${
template.display_name
? template.display_name ? template.display_name
: toNormalCase(template[t].name) : toNormalCase(template[t].name)
}.`, }.`,
@ -298,6 +303,12 @@ export default function ChatModal({
const ref = useRef(null); const ref = useRef(null);
useEffect(() => {
if (open && ref.current) {
ref.current.focus();
}
}, [open]);
function sendMessage() { function sendMessage() {
if (chatValue !== "") { if (chatValue !== "") {
let nodeValidationErrors = validateNodes(); let nodeValidationErrors = validateNodes();
@ -382,7 +393,9 @@ export default function ChatModal({
</div> </div>
<div className="w-full h-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center overflow-scroll scrollbar-hide"> <div className="w-full h-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center overflow-scroll scrollbar-hide">
{chatHistory.length > 0 ? ( {chatHistory.length > 0 ? (
chatHistory.map((c, i) => <ChatMessage lockChat={lockChat} chat={c} key={i} />) chatHistory.map((c, i) => (
<ChatMessage lockChat={lockChat} chat={c} key={i} />
))
) : ( ) : (
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle"> <div className="flex flex-col h-full text-center justify-center w-full items-center align-middle">
<span> <span>
@ -412,6 +425,7 @@ export default function ChatModal({
lockChat={lockChat} lockChat={lockChat}
sendMessage={sendMessage} sendMessage={sendMessage}
setChatValue={setChatValue} setChatValue={setChatValue}
inputRef={ref}
/> />
</div> </div>
</div> </div>

View file

@ -11,6 +11,7 @@ import "ace-builds/src-noconflict/ext-language_tools";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
import { checkCode } from "../../controllers/API"; import { checkCode } from "../../controllers/API";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
export default function CodeAreaModal({ export default function CodeAreaModal({
value, value,
setValue, setValue,
@ -22,6 +23,7 @@ export default function CodeAreaModal({
const [code, setCode] = useState(value); const [code, setCode] = useState(value);
const { dark } = useContext(darkContext); const { dark } = useContext(darkContext);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const { setDisableCopyPaste } = useContext(TabsContext);
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const ref = useRef(); const ref = useRef();
function setModalOpen(x: boolean) { function setModalOpen(x: boolean) {
@ -109,6 +111,12 @@ export default function CodeAreaModal({
onChange={(value) => { onChange={(value) => {
setCode(value); setCode(value);
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
className="h-full w-full rounded-lg" className="h-full w-full rounded-lg"
/> />
</div> </div>

View file

@ -16,7 +16,7 @@ export default function ExportModal() {
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const ref = useRef(); const ref = useRef();
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { flows, tabIndex, updateFlow, downloadFlow,setDisableCP } = useContext(TabsContext); const { flows, tabIndex, updateFlow, downloadFlow,setDisableCopyPaste } = useContext(TabsContext);
function setModalOpen(x: boolean) { function setModalOpen(x: boolean) {
setOpen(x); setOpen(x);
if (x === false) { if (x === false) {
@ -114,10 +114,10 @@ export default function ExportModal() {
id="name" id="name"
className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500 text-gray-900 dark:text-gray-100" className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500 text-gray-900 dark:text-gray-100"
onBlur={() => { onBlur={() => {
setDisableCP(false); setDisableCopyPaste(false);
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true); setDisableCopyPaste(true);
}} }}
/> />
</div> </div>
@ -134,10 +134,10 @@ export default function ExportModal() {
</label> </label>
<textarea <textarea
onBlur={() => { onBlur={() => {
setDisableCP(false); setDisableCopyPaste(false);
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true); setDisableCopyPaste(true);
}} }}
name="description" name="description"
id="description" id="description"
@ -164,10 +164,10 @@ export default function ExportModal() {
type="checkbox" type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500" className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
onBlur={() => { onBlur={() => {
setDisableCP(false); setDisableCopyPaste(false);
}} }}
onFocus={() => { onFocus={() => {
setDisableCP(true); setDisableCopyPaste(true);
}} }}
/> />
<span className="ml-2 font-medium text-gray-700 dark:text-white"> <span className="ml-2 font-medium text-gray-700 dark:text-white">

View file

@ -1,4 +1,4 @@
import React, { ReactNode } from "react"; import React, { ReactNode, useEffect } from "react";
import { DocumentDuplicateIcon } from "@heroicons/react/solid"; import { DocumentDuplicateIcon } from "@heroicons/react/solid";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import Tooltip from "../../../components/TooltipComponent"; import Tooltip from "../../../components/TooltipComponent";
@ -30,6 +30,8 @@ export default function ButtonBox({
let marginTop: string; let marginTop: string;
let height: string; let height: string;
let width: string; let width: string;
let textHeight: number;
let textWidth: number;
switch (size) { switch (size) {
case "small": case "small":
bigCircle = "h-12 w-12"; bigCircle = "h-12 w-12";
@ -39,6 +41,8 @@ export default function ButtonBox({
padding = "p-2 py-3"; padding = "p-2 py-3";
marginTop = "mt-2"; marginTop = "mt-2";
height = "h-36"; height = "h-36";
textHeight = 70;
textWidth = 80;
width = "w-32"; width = "w-32";
break; break;
case "medium": case "medium":
@ -48,6 +52,8 @@ export default function ButtonBox({
descriptionFontSize = "text-sm"; descriptionFontSize = "text-sm";
padding = "p-4 py-5"; padding = "p-4 py-5";
marginTop = "mt-3"; marginTop = "mt-3";
textHeight = 112;
textWidth = 162;
height = "h-44"; height = "h-44";
width = "w-36"; width = "w-36";
break; break;
@ -72,6 +78,45 @@ export default function ButtonBox({
width = "w-44"; width = "w-44";
break; break;
} }
const titleRef = React.useRef<HTMLHeadingElement>(null);
useEffect(() => {
const resizeFont = () => {
const titleElement = titleRef.current;
if (titleElement) {
const containerWidth = titleElement.offsetWidth;
const containerHeight = titleElement.offsetHeight;
const titleComputedStyle = window.getComputedStyle(titleElement);
const titleWidth = titleElement.getBoundingClientRect().width;
const currentFontSize = parseFloat(titleComputedStyle.fontSize);
const desiredWidth = textWidth - 10; // Subtracting the desired padding
// Calculate the desired font size based on the adjusted width
let desiredFontSize = currentFontSize * (desiredWidth / titleWidth);
// Adjust the desired font size to fit within the container height, if needed
const maxHeight = containerHeight - 10; // Subtracting the desired top padding
const maxHeightFontSize = maxHeight * 0.8; // Adjust the scaling factor as needed
desiredFontSize = Math.min(desiredFontSize, maxHeightFontSize);
// Apply the desired font size and padding to the title element
titleElement.style.fontSize = `${desiredFontSize}px`;
titleElement.style.paddingLeft = "5px";
titleElement.style.paddingRight = "5px";
}
};
resizeFont();
window.addEventListener("resize", resizeFont);
return () => {
window.removeEventListener("resize", resizeFont);
};
}, []);
return ( return (
<button disabled={deactivate} onClick={onClick}> <button disabled={deactivate} onClick={onClick}>
<div <div
@ -92,18 +137,17 @@ export default function ButtonBox({
<div className={textColor}>{icon}</div> <div className={textColor}>{icon}</div>
</div> </div>
</div> </div>
<div className="w-full mt-auto mb-auto">
<h3 <h3
ref={titleRef}
className={classNames( className={classNames(
"w-full font-semibold break-words text-white dark:text-white/80", " font-semibold text-white dark:text-white/80",
titleFontSize,
marginTop marginTop
)} )}
> >
{title} {title}
</h3> </h3>
</div> </div>
</div>
</button> </button>
); );
} }

View file

@ -48,7 +48,7 @@ export default function ExtraSidebar() {
} }
> >
<div className="flex w-full justify-between text-sm px-3 py-1 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border"> <div className="flex w-full justify-between text-sm px-3 py-1 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border">
<span className="text-black dark:text-white w-36 truncate text-xs"> <span className="text-black dark:text-white w-36 pr-1 truncate text-xs">
{t} {t}
</span> </span>
<Bars2Icon className="w-4 h-6 text-gray-400 dark:text-gray-600" /> <Bars2Icon className="w-4 h-6 text-gray-400 dark:text-gray-600" />

View file

@ -14,7 +14,7 @@ export default function TabComponent({
selected: boolean; selected: boolean;
onClick: () => void; onClick: () => void;
}) { }) {
const { removeFlow, updateFlow, flows, setDisableCP } = useContext(TabsContext); const { removeFlow, updateFlow, flows, setDisableCopyPaste } = useContext(TabsContext);
const [isRename, setIsRename] = useState(false); const [isRename, setIsRename] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
return ( return (
@ -41,14 +41,14 @@ export default function TabComponent({
{isRename ? ( {isRename ? (
<input <input
onFocus={() => { onFocus={() => {
setDisableCP(true); setDisableCopyPaste(true);
}} }}
autoFocus autoFocus
className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28" className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28"
onBlur={() => { onBlur={() => {
setIsRename(false); setIsRename(false);
setDisableCopyPaste(false);
if (value !== "") { if (value !== "") {
setDisableCP(false);
let newFlow = _.cloneDeep(flow); let newFlow = _.cloneDeep(flow);
newFlow.name = value; newFlow.name = value;
updateFlow(newFlow); updateFlow(newFlow);

View file

@ -11,11 +11,13 @@ import ReactFlow, {
Connection, Connection,
Edge, Edge,
useKeyPress, useKeyPress,
useOnSelectionChange,
NodeDragHandler, NodeDragHandler,
OnEdgesDelete, OnEdgesDelete,
OnNodesDelete, OnNodesDelete,
SelectionDragHandler, SelectionDragHandler,
useOnViewportChange,
OnSelectionChangeParams,
OnNodesChange,
} from "reactflow"; } from "reactflow";
import _ from "lodash"; import _ from "lodash";
import { locationContext } from "../../contexts/locationContext"; import { locationContext } from "../../contexts/locationContext";
@ -35,9 +37,8 @@ const nodeTypes = {
genericNode: GenericNode, genericNode: GenericNode,
}; };
export default function FlowPage({ flow }: { flow: FlowType }) { export default function FlowPage({ flow }: { flow: FlowType }) {
let { updateFlow, incrementNodeId, disableCP} = let { updateFlow, disableCopyPaste, addFlow, getNodeId, paste } =
useContext(TabsContext); useContext(TabsContext);
const { types, reactFlowInstance, setReactFlowInstance, templates } = const { types, reactFlowInstance, setReactFlowInstance, templates } =
useContext(typesContext); useContext(typesContext);
@ -45,87 +46,62 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo(); const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo();
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { const [position, setPosition] = useState({ x: 0, y: 0 });
if ((event.ctrlKey || event.metaKey) && (event.key === 'c') && lastSelection && !disableCP) { const [lastSelection, setLastSelection] =
event.preventDefault(); useState<OnSelectionChangeParams>(null);
setLastCopiedSelection(lastSelection);
}
if ((event.ctrlKey || event.metaKey) && (event.key === 'v') && lastCopiedSelection && !disableCP) {
event.preventDefault();
paste();
}
}
const [lastSelection, setLastSelection] = useState(null);
const [lastCopiedSelection, setLastCopiedSelection] = useState(null); const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() => {
// this effect is used to attach the global event handlers
const onKeyDown = (event: KeyboardEvent) => {
console.log("keydownou", lastCopiedSelection, position);
if (
(event.ctrlKey || event.metaKey) &&
event.key === "c" &&
lastSelection &&
!disableCopyPaste
) {
event.preventDefault();
setLastCopiedSelection(_.cloneDeep(lastSelection));
}
if (
(event.ctrlKey || event.metaKey) &&
event.key === "v" &&
lastCopiedSelection &&
!disableCopyPaste
) {
event.preventDefault();
let bounds = reactFlowWrapper.current.getBoundingClientRect();
paste(lastCopiedSelection, {
x: position.x - bounds.left,
y: position.y - bounds.top,
});
}
if (
(event.ctrlKey || event.metaKey) &&
event.key === "g" &&
lastSelection
) {
event.preventDefault();
// addFlow(newFlow, false);
}
};
const handleMouseMove = (event) => { const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY }); setPosition({ x: event.clientX, y: event.clientY });
}; };
useOnSelectionChange({ document.addEventListener("keydown", onKeyDown);
onChange: (flow) => { setLastSelection(flow); }, document.addEventListener("mousemove", handleMouseMove);
})
let paste = () => { return () => {
let minimumX = Infinity; document.removeEventListener("keydown", onKeyDown);
let minimumY = Infinity; document.removeEventListener("mousemove", handleMouseMove);
let idsMap = {};
lastCopiedSelection.nodes.forEach((n) => {
if (n.position.y < minimumY) {
minimumY = n.position.y
}
if (n.position.x < minimumX) {
minimumX = n.position.x;
}
});
const bounds = reactFlowWrapper.current.getBoundingClientRect();
const insidePosition = reactFlowInstance.project({
x: position.x - bounds.left,
y: position.y - bounds.top
});
lastCopiedSelection.nodes.forEach((n) => {
// Generate a unique node ID
let newId = getId();
idsMap[n.id] = newId;
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
position: {
x: insidePosition.x + n.position.x - minimumX,
y: insidePosition.y + n.position.y - minimumY,
},
data: {
...n.data,
id: newId,
},
}; };
}, [position, lastCopiedSelection, lastSelection]);
// Add the new node to the list of nodes in state const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
setNodes((nds) => nds.map((e) => ({ ...e, selected: false })).concat({ ...newNode, selected: false }));
})
lastCopiedSelection.edges.forEach((e) => {
let source = idsMap[e.source];
let target = idsMap[e.target];
let sourceHandleSplitted = e.sourceHandle.split('|');
let sourceHandle = sourceHandleSplitted[0] + '|' + source + '|' + sourceHandleSplitted.slice(2).join('|');
let targetHandleSplitted = e.targetHandle.split('|');
let targetHandle = targetHandleSplitted.slice(0, -1).join('|') + '|' + target;
let id = "reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle;
setEdges((eds) =>
addEdge({ source, target, sourceHandle, targetHandle, id, className: "animate-pulse", selected: false }, eds.map((e) => ({ ...e, selected: false })))
);
})
}
const { setExtraComponent, setExtraNavigation } = useContext(locationContext); const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
@ -137,11 +113,6 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
); );
const { setViewport } = useReactFlow(); const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true); const edgeUpdateSuccessful = useRef(true);
function getId() {
return `dndnode_` + incrementNodeId();
}
useEffect(() => { useEffect(() => {
if (reactFlowInstance && flow) { if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject(); flow.data = reactFlowInstance.toObject();
@ -178,7 +149,21 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
(params: Connection) => { (params: Connection) => {
takeSnapshot(); takeSnapshot();
setEdges((eds) => setEdges((eds) =>
addEdge({ ...params, className: "animate-pulse" }, eds) addEdge(
{
...params,
style:
params.targetHandle.split("|")[0] === "Text"
? { stroke: "#333333", strokeWidth: 2 }
: { stroke: "#222222" },
className:
params.targetHandle.split("|")[0] === "Text"
? ""
: "animate-pulse",
animated: params.targetHandle.split("|")[0] === "Text",
},
eds
)
); );
setNodes((x) => { setNodes((x) => {
let newX = _.cloneDeep(x); let newX = _.cloneDeep(x);
@ -199,7 +184,6 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
takeSnapshot(); takeSnapshot();
}, [takeSnapshot]); }, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => { const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable // 👇 make deleting edges undoable
takeSnapshot(); takeSnapshot();
@ -224,11 +208,6 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
); );
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node // If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
if (
data.type !== "chatInput" ||
(data.type === "chatInput" &&
!reactFlowInstance.getNodes().some((n) => n.type === "chatInputNode"))
) {
// Calculate the position where the node should be created // Calculate the position where the node should be created
const position = reactFlowInstance.project({ const position = reactFlowInstance.project({
x: event.clientX - reactflowBounds.left, x: event.clientX - reactflowBounds.left,
@ -236,10 +215,24 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
}); });
// Generate a unique node ID // Generate a unique node ID
let newId = getId(); let newId = getNodeId();
let newNode: NodeType;
if (data.type !== "groupNode") {
// Create a new node object // Create a new node object
const newNode: NodeType = { newNode = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
} else {
// Create a new node object
newNode = {
id: newId, id: newId,
type: "genericNode", type: "genericNode",
position, position,
@ -251,27 +244,24 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
}; };
// Add the new node to the list of nodes in state // Add the new node to the list of nodes in state
setNodes((nds) => nds.concat(newNode));
} else {
// If a chat input node already exists, set an error message
setErrorData({
title: "Error creating node",
list: ["There can't be more than one chat input."],
});
} }
setNodes((nds) => nds.concat(newNode));
}, },
// Specify dependencies for useCallback // Specify dependencies for useCallback
[incrementNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot] [getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
); );
const onDelete = useCallback((mynodes) => { const onDelete = useCallback(
(mynodes) => {
takeSnapshot(); takeSnapshot();
setEdges( setEdges(
edges.filter( edges.filter(
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id) (ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
) )
); );
}, [takeSnapshot, edges, setEdges]); },
[takeSnapshot, edges, setEdges]
);
const onEdgeUpdateStart = useCallback(() => { const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false; edgeUpdateSuccessful.current = false;
@ -295,19 +285,40 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
edgeUpdateSuccessful.current = true; edgeUpdateSuccessful.current = true;
}, []); }, []);
const [selectionEnded, setSelectionEnded] = useState(false);
const onSelectionEnd = useCallback(() => {
setSelectionEnded(true);
}, []);
const onSelectionStart = useCallback(() => {
setSelectionEnded(false);
}, []);
// Workaround to show the menu only after the selection has ended.
useEffect(() => {
if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) {
setSelectionMenuVisible(true);
} else {
setSelectionMenuVisible(false);
}
}, [selectionEnded, lastSelection]);
const onSelectionChange = useCallback((flow) => {
setLastSelection(flow);
}, []);
return ( return (
<div className="w-full h-full" onMouseMove={handleMouseMove} ref={reactFlowWrapper}> <div className="w-full h-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? ( {Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
<> <>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
onMove={() => onMove={() => {
updateFlow({ ...flow, data: reactFlowInstance.toObject() }) updateFlow({ ...flow, data: reactFlowInstance.toObject() });
} }}
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChangeMod} onEdgesChange={onEdgesChangeMod}
onKeyDown={(e) => onKeyDown(e)}
onConnect={onConnect} onConnect={onConnect}
onLoad={setReactFlowInstance} onLoad={setReactFlowInstance}
onInit={setReactFlowInstance} onInit={setReactFlowInstance}
@ -317,17 +328,18 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart} onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart}
onEdgesDelete={onEdgesDelete} onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent} connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={onDrop} onDrop={onDrop}
onNodesDelete={onDelete} onNodesDelete={onDelete}
onSelectionChange={onSelectionChange}
selectNodesOnDrag={false} selectNodesOnDrag={false}
className="theme-attribution"
> >
<Background className="dark:bg-gray-900" /> <Background className="dark:bg-gray-900" />
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"> <Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"></Controls>
</Controls>
</ReactFlow> </ReactFlow>
<Chat flow={flow} reactFlowInstance={reactFlowInstance} /> <Chat flow={flow} reactFlowInstance={reactFlowInstance} />
</> </>

View file

@ -6,15 +6,17 @@ export type TabsContextType = {
setTabIndex: (index: number) => void; setTabIndex: (index: number) => void;
flows: Array<FlowType>; flows: Array<FlowType>;
removeFlow: (id: string) => void; removeFlow: (id: string) => void;
addFlow: (flowData?: FlowType) => void; addFlow: (flowData?: FlowType,newFlow?:boolean) => void;
updateFlow: (newFlow: FlowType) => void; updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => number; incrementNodeId: () => string;
downloadFlow: (flow: FlowType) => void; downloadFlow: (flow: FlowType) => void;
uploadFlow: () => void; uploadFlow: (newFlow?:boolean) => void;
hardReset: () => void; hardReset: () => void;
//disable CopyPaste //disable CopyPaste
disableCP: boolean; disableCopyPaste: boolean;
setDisableCP: (value: boolean) => void; setDisableCopyPaste: (value: boolean) => void;
getNodeId: () => string;
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => void;
}; };
export type LangFlowState = { export type LangFlowState = {

View file

@ -101,7 +101,7 @@ export const nodeNames: { [char: string]: string } = {
advanced: "Advanced", advanced: "Advanced",
chat: "Chat", chat: "Chat",
embeddings: "Embeddings", embeddings: "Embeddings",
documentloaders: "Document Loaders", documentloaders: "Loaders",
vectorstores: "Vector Stores", vectorstores: "Vector Stores",
toolkits: "Toolkits", toolkits: "Toolkits",
wrappers: "Wrappers", wrappers: "Wrappers",

View file

@ -84,7 +84,6 @@ module.exports = {
}, },
}); });
}), }),
require("@tailwindcss/line-clamp"),
require("@tailwindcss/typography"), require("@tailwindcss/typography"),
], ],
}; };