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 }}
run: |
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
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:
make install_frontend
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)
### 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

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

View file

@ -33,11 +33,15 @@ def serve(
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
log_level: str = typer.Option("info", help="Logging level."),
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.
"""
if jcloud:
return serve_on_jcloud()
configure(log_level=log_level, log_file=log_file)
update_settings(config)
app = create_app()
@ -69,6 +73,52 @@ def serve(
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():
app()

View file

@ -1,5 +1,5 @@
from importlib.metadata import version
import logging
from importlib.metadata import version
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())})
except Exception as 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
# show how many documents are in the list?
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}()"

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.custom_lists import embedding_type_to_cls_dict
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.util import build_template_from_class
@ -14,6 +16,10 @@ class EmbeddingCreator(LangChainTypeCreator):
def type_to_loader_dict(self) -> 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]:
"""Get the signature of an embedding."""
try:

View file

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

View file

@ -3,12 +3,12 @@ import io
from typing import Any, Dict, List, Tuple
from chromadb.errors import NotEnoughElementsException # type: ignore
from langchain.schema import AgentAction
from langflow.api.callback import AsyncStreamingLLMCallbackHandler, StreamingLLMCallbackHandler # type: ignore
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict
from langflow.graph.graph import Graph
from langflow.utils.logger import logger
from langchain.schema import AgentAction
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,
multiline=False,
name="llm",
display_name="LLM",
),
TemplateField(
field_type="BaseChatMemory",
@ -156,6 +157,7 @@ class TimeTravelGuideChainNode(FrontendNode):
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
TemplateField(
field_type="BaseChatMemory",
@ -210,6 +212,7 @@ class SeriesCharacterChainNode(FrontendNode):
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
],
)
@ -295,6 +298,7 @@ class JsonAgentNode(FrontendNode):
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
@ -341,6 +345,7 @@ class InitializeAgentNode(FrontendNode):
required=True,
show=True,
name="llm",
display_name="LLM",
advanced=False,
),
],
@ -376,6 +381,7 @@ class CSVAgentNode(FrontendNode):
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
@ -614,3 +620,11 @@ class LLMFrontendNode(FrontendNode):
elif field.name in ["model_name", "temperature"]:
field.advanced = False
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,153 +16,162 @@ import CrashErrorComponent from "./components/CrashErrorComponent";
import { TabsContext } from "./contexts/tabsContext";
export default function App() {
let { setCurrent, setShowSideBar, setIsStackedOpen } =
useContext(locationContext);
let location = useLocation();
useEffect(() => {
setCurrent(location.pathname.replace(/\/$/g, "").split("/"));
setShowSideBar(true);
setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext);
const {
errorData,
errorOpen,
setErrorOpen,
noticeData,
noticeOpen,
setNoticeOpen,
successData,
successOpen,
setSuccessOpen,
} = useContext(alertContext);
let { setCurrent, setShowSideBar, setIsStackedOpen } =
useContext(locationContext);
let location = useLocation();
useEffect(() => {
setCurrent(location.pathname.replace(/\/$/g, "").split("/"));
setShowSideBar(true);
setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext);
const {
errorData,
errorOpen,
setErrorOpen,
noticeData,
noticeOpen,
setNoticeOpen,
successData,
successOpen,
setSuccessOpen,
} = useContext(alertContext);
// Initialize state variable for the list of alerts
const [alertsList, setAlertsList] = useState<
Array<{
type: string;
data: { title: string; list?: Array<string>; link?: string };
id: string;
}>
>([]);
// Initialize state variable for the list of alerts
const [alertsList, setAlertsList] = useState<
Array<{
type: string;
data: { title: string; list?: Array<string>; link?: string };
id: string;
}>
>([]);
// 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
useEffect(() => {
// If there is an error alert open with data, add it to the alertsList
if (errorOpen && errorData) {
setErrorOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
];
return newAlertsList;
});
}
// If there is a notice alert open with data, add it to the alertsList
else if (noticeOpen && noticeData) {
setNoticeOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
];
return newAlertsList;
});
}
// If there is a success alert open with data, add it to the alertsList
else if (successOpen && successData) {
setSuccessOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
];
return newAlertsList;
});
}
}, [
_,
errorData,
errorOpen,
noticeData,
noticeOpen,
setErrorOpen,
setNoticeOpen,
setSuccessOpen,
successData,
successOpen,
]);
// Use effect hook to update alertsList when a new alert is added
useEffect(() => {
// If there is an error alert open with data, add it to the alertsList
if (errorOpen && errorData) {
setErrorOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
];
return newAlertsList;
});
}
// If there is a notice alert open with data, add it to the alertsList
else if (noticeOpen && noticeData) {
setNoticeOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
];
return newAlertsList;
});
}
// If there is a success alert open with data, add it to the alertsList
else if (successOpen && successData) {
setSuccessOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
];
return newAlertsList;
});
}
}, [
_,
errorData,
errorOpen,
noticeData,
noticeOpen,
setErrorOpen,
setNoticeOpen,
setSuccessOpen,
successData,
successOpen,
]);
const removeAlert = (id: string) => {
setAlertsList((prevAlertsList) =>
prevAlertsList.filter((alert) => alert.id !== id)
);
};
const removeAlert = (id: string) => {
setAlertsList((prevAlertsList) =>
prevAlertsList.filter((alert) => alert.id !== id)
);
};
return (
//need parent component with width and height
<div className="h-full flex flex-col">
<div className="flex grow-0 shrink basis-auto"></div>
<ErrorBoundary
onReset={() => {
window.localStorage.removeItem("tabsData");
window.localStorage.clear();
hardReset();
window.location.href = window.location.href;
}}
FallbackComponent={CrashErrorComponent}
>
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
<ExtraSidebar />
{/* Main area */}
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
{/* Primary column */}
<div className="w-full h-full">
<TabsManagerComponent></TabsManagerComponent>
</div>
</main>
</div>
</ErrorBoundary>
<div></div>
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
{alertsList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.data.title}
list={alert.data.list}
id={alert.id}
removeAlert={removeAlert}
/>
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.data.title}
link={alert.data.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
<SuccessAlert
key={alert.id}
title={alert.data.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<a
target={"_blank"}
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 "
>
Created by Logspace
</a>
</div>
);
return (
//need parent component with width and height
<div className="h-full flex flex-col">
<div className="flex grow-0 shrink basis-auto"></div>
<ErrorBoundary
onReset={() => {
window.localStorage.removeItem("tabsData");
window.localStorage.clear();
hardReset();
window.location.href = window.location.href;
}}
FallbackComponent={CrashErrorComponent}
>
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
<ExtraSidebar />
{/* Main area */}
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
{/* Primary column */}
<div className="w-full h-full">
<TabsManagerComponent></TabsManagerComponent>
</div>
</main>
</div>
</ErrorBoundary>
<div></div>
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
{alertsList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.data.title}
list={alert.data.list}
id={alert.id}
removeAlert={removeAlert}
/>
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.data.title}
link={alert.data.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
<SuccessAlert
key={alert.id}
title={alert.data.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<a
target={"_blank"}
href="https://logspace.ai/"
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"
>
{version && <div className="mt-1"> LangFlow v{version}</div>}
<div className="mt-2">Created by Logspace</div>
</a>
</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 ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext";
@ -12,11 +20,11 @@ import { TabsContext } from "../../contexts/tabsContext";
import { debounce } from "../../utils";
import Tooltip from "../../components/TooltipComponent";
export default function GenericNode({
data,
selected,
data,
selected,
}: {
data: NodeDataType;
selected: boolean;
data: NodeDataType;
selected: boolean;
}) {
const { setErrorData } = useContext(alertContext);
const showError = useRef(true);
@ -30,32 +38,28 @@ export default function GenericNode({
const { reactFlowInstance } = useContext(typesContext);
const [params, setParams] = useState([]);
useEffect(() => {
if (reactFlowInstance) {
setParams(Object.values(reactFlowInstance.toObject()));
}
}, [save]);
useEffect(() => {
if (reactFlowInstance) {
setParams(Object.values(reactFlowInstance.toObject()));
}
}, [save]);
const validateNode = useCallback(
debounce(async () => {
try {
const response = await fetch(`/validate/node/${data.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reactFlowInstance.toObject()),
});
const validateNode = useCallback(
debounce(async () => {
try {
const response = await fetch(`/validate/node/${data.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reactFlowInstance.toObject()),
});
if (response.status === 200) {
let jsonResponse = await response.json();
let jsonResponseParsed = await JSON.parse(jsonResponse);
console.log(jsonResponseParsed);
if(jsonResponseParsed.valid){
setValidationStatus(jsonResponseParsed.params);
} else {
setValidationStatus("error");
}
setValidationStatus(jsonResponseParsed);
}
} catch (error) {
// console.error("Error validating node:", error);
@ -70,31 +74,22 @@ export default function GenericNode({
}
}, [params, validateNode]);
useEffect(() => {
if (validationStatus !== "error") {
setIsValid(true);
} else {
setIsValid(false);
if (!Icon) {
if (showError.current) {
setErrorData({
title: data.type
? `The ${data.type} node could not be rendered, please review your json file`
: "There was a node that can't be rendered, please review your json file",
});
showError.current = false;
}
}, [validationStatus]);
if (!Icon) {
if (showError.current) {
setErrorData({
title: data.type
? `The ${data.type} node could not be rendered, please review your json file`
: "There was a node that can't be rendered, please review your json file",
});
showError.current = false;
}
deleteNode(data.id);
return;
}
deleteNode(data.id);
return;
}
return (
<div
className={classNames(
isValid ? "animate-pulse-green" : "border-red-outline",
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"
)}
@ -108,15 +103,48 @@ export default function GenericNode({
}}
/>
<div className="ml-2 truncate">{data.type}</div>
{validationStatus && validationStatus !== "error" ?
<Tooltip title={
<div className="max-h-96 overflow-auto">
{validationStatus}
</div>}>
<ExclamationCircleIcon className="w-5 hover:text-gray-500 hover:dark:text-gray-300" />
</Tooltip>
: <></>
}
<div>
<Tooltip
title={
!validationStatus ? (
"Validating..."
) : (
<div className="max-h-96 overflow-auto">
{validationStatus.params.split("\n").map((line, index) => (
<div key={index}>{line}</div>
))}
</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>
</div>
</div>
<div className="flex gap-3">
<button
@ -162,12 +190,12 @@ export default function GenericNode({
{data.node.description}
</div>
<>
{Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_")
.map((t: string, idx) => (
<div key={idx}>
{/* {idx === 0 ? (
<>
{Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_")
.map((t: string, idx) => (
<div key={idx}>
{/* {idx === 0 ? (
<div
className={classNames(
"px-5 py-2 mt-2 dark:text-white text-center",
@ -186,59 +214,59 @@ export default function GenericNode({
) : (
<></>
)} */}
{data.node.template[t].show &&
!data.node.template[t].advanced ? (
<ParameterComponent
data={data}
color={
nodeColors[types[data.node.template[t].type]] ??
nodeColors.unknown
}
title={
data.node.template[t].display_name
? data.node.template[t].display_name
: data.node.template[t].name
? toNormalCase(data.node.template[t].name)
: toNormalCase(t)
}
name={t}
tooltipTitle={
"Type: " +
data.node.template[t].type +
(data.node.template[t].list ? " list" : "")
}
required={data.node.template[t].required}
id={data.node.template[t].type + "|" + t + "|" + data.id}
left={true}
type={data.node.template[t].type}
/>
) : (
<></>
)}
</div>
))}
<div
className={classNames(
Object.keys(data.node.template).length < 1 ? "hidden" : "",
"w-full flex justify-center"
)}
>
{" "}
</div>
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
{data.node.template[t].show &&
!data.node.template[t].advanced ? (
<ParameterComponent
data={data}
color={
nodeColors[types[data.node.template[t].type]] ??
nodeColors.unknown
}
title={
data.node.template[t].display_name
? data.node.template[t].display_name
: data.node.template[t].name
? toNormalCase(data.node.template[t].name)
: toNormalCase(t)
}
name={t}
tooltipTitle={
"Type: " +
data.node.template[t].type +
(data.node.template[t].list ? " list" : "")
}
required={data.node.template[t].required}
id={data.node.template[t].type + "|" + t + "|" + data.id}
left={true}
type={data.node.template[t].type}
/>
) : (
<></>
)}
</div>
))}
<div
className={classNames(
Object.keys(data.node.template).length < 1 ? "hidden" : "",
"w-full flex justify-center"
)}
>
{" "}
</div>
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
Output
</div> */}
<ParameterComponent
data={data}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={data.type}
tooltipTitle={`Type: ${data.node.base_classes.join(" | ")}`}
id={[data.type, data.id, ...data.node.base_classes].join("|")}
type={data.node.base_classes.join("|")}
left={false}
/>
</>
</div>
</div>
);
<ParameterComponent
data={data}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={data.type}
tooltipTitle={`Type: ${data.node.base_classes.join(" | ")}`}
id={[data.type, data.id, ...data.node.base_classes].join("|")}
type={data.node.base_classes.join("|")}
left={false}
/>
</>
</div>
</div>
);
}

View file

@ -21,10 +21,10 @@ export default function ChatTrigger({ open, setOpen }) {
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="absolute bottom-6 right-3">
<div className="absolute bottom-4 right-3">
<div
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"
// style={{ backgroundColor: nodeColors["chat"] }}
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
onClick={() => {

View file

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

View file

@ -11,7 +11,7 @@ export default function InputComponent({
}: InputComponentType) {
const [myValue, setMyValue] = useState(value ?? "");
const [pwdVisible, setPwdVisible] = useState(false);
const {setDisableCP} = useContext(TabsContext)
const {setDisableCopyPaste} = useContext(TabsContext)
useEffect(() => {
if (disabled) {
setMyValue("");
@ -29,10 +29,10 @@ export default function InputComponent({
<input
value={myValue}
onBlur={() => {
setDisableCP(false)
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCP(true)
setDisableCopyPaste(true)
}}
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",

View file

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

View file

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

View file

@ -21,9 +21,9 @@ export default function ToggleComponent({
setEnabled(x);
}}
className={classNames(
enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-600",
"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 "
)}
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 focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2'
)}
>
<span className="sr-only">Use setting</span>
<span
@ -44,19 +44,6 @@ export default function ToggleComponent({
)}
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
className={classNames(
@ -67,13 +54,6 @@ export default function ToggleComponent({
)}
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>
</Switch>

View file

@ -6,13 +6,14 @@ import {
ReactNode,
useContext,
} from "react";
import { FlowType } from "../types/flow";
import { FlowType, NodeType } from "../types/flow";
import { LangFlowState, TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils";
import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid";
import { addEdge } from "reactflow";
const TabsContextInitialValue: TabsContextType = {
save: () => {},
@ -22,12 +23,14 @@ const TabsContextInitialValue: TabsContextType = {
removeFlow: (id: string) => {},
addFlow: (flowData?: any) => {},
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => 0,
incrementNodeId: () => uuidv4(),
downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {},
hardReset: () => {},
disableCP:false,
setDisableCP:(state:boolean)=>{},
disableCopyPaste:false,
setDisableCopyPaste:(state:boolean)=>{},
getNodeId: () => "",
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => {},
};
export const TabsContext = createContext<TabsContextType>(
@ -39,24 +42,24 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const [tabIndex, setTabIndex] = useState(0);
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uuidv4());
const { templates } = useContext(typesContext);
const { templates, reactFlowInstance } = useContext(typesContext);
const newNodeId = useRef(0);
const newNodeId = useRef(uuidv4());
function incrementNodeId() {
newNodeId.current = newNodeId.current + 1;
newNodeId.current = uuidv4();
return newNodeId.current;
}
function save() {
if (flows.length !== 0)
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
JSON.stringify({ tabIndex, flows, id})
);
}
useEffect(() => {
//save tabs locally
// console.log(id)
save();
}, [flows, id, tabIndex, newNodeId]);
useEffect(() => {
@ -80,12 +83,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows);
setId(cookieObject.id);
newNodeId.current = cookieObject.nodeId;
}
}, [templates]);
function hardReset() {
newNodeId.current = 0;
newNodeId.current = uuidv4();
setTabIndex(0);
setFlows([]);
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.
* 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.
* @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) {
// Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null;
@ -192,6 +282,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
setId(uuidv4());
// Add the new flow to the list of flows.
setFlows((prevState) => {
const newFlows = [...prevState, newFlow];
return newFlows;
@ -216,13 +307,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return newFlows;
});
}
const [disableCP, setDisableCP] = useState(false);
const [disableCopyPaste, setDisableCopyPaste] = useState(false);
return (
<TabsContext.Provider
value={{
disableCP,
setDisableCP,
disableCopyPaste,
setDisableCopyPaste,
save,
hardReset,
tabIndex,
@ -234,9 +325,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
updateFlow,
downloadFlow,
uploadFlow,
getNodeId,
paste,
}}
>
{children}
</TabsContext.Provider>
);
}
}

View file

@ -7,83 +7,116 @@ import { APIKindType } from "../types/api";
//context to share types adn functions from nodes to flow
const initialValue: typesContextType = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {},
types: {},
setTypes: () => {},
templates: {},
setTemplates: () => {},
data: {},
setData: () => {},
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {},
types: {},
setTypes: () => {},
templates: {},
setTemplates: () => {},
data: {},
setData: () => {},
};
export const typesContext = createContext<typesContextType>(initialValue);
export function TypesProvider({ children }: { children: ReactNode }) {
const [types, setTypes] = useState({});
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
const [types, setTypes] = useState({});
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
useEffect(() => {
async function getTypes(): Promise<void> {
// Make an asynchronous API call to retrieve all data.
let result = await getAll();
useEffect(() => {
let delay = 1000; // Start delay of 1 second
let intervalId = null;
let retryCount = 0; // Count of retry attempts
const maxRetryCount = 5; // Max retry attempts
// Update the state of the component with the retrieved data.
setData(result.data);
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = result.data[curr][c];
});
return acc;
}, {})
);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Call the getTypes function.
getTypes();
}, [setTypes]);
// We will keep a flag to handle the case where the component is unmounted before the API call resolves.
let isMounted = true;
function deleteNode(idx: string) {
reactFlowInstance.setNodes(
reactFlowInstance.getNodes().filter((n: Node) => n.id !== idx)
);
reactFlowInstance.setEdges(
reactFlowInstance
.getEdges()
.filter((ns) => ns.source !== idx && ns.target !== idx)
);
}
return (
<typesContext.Provider
value={{
types,
setTypes,
reactFlowInstance,
setReactFlowInstance,
deleteNode,
setTemplates,
templates,
data,
setData,
}}
>
{children}
</typesContext.Provider>
);
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);
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = result.data[curr][c];
});
return acc;
}, {})
);
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {})
);
}
// Clear the interval if successful.
clearInterval(intervalId);
} catch (error) {
retryCount++;
// On error, double the delay for the next attempt up to a maximum.
delay = Math.min(30000, delay * 2);
// Log errors but don't do anything else - the function will try again on the next interval.
console.error(error);
// Clear the old interval and start a new one with the new delay.
if (retryCount <= maxRetryCount) {
clearInterval(intervalId);
intervalId = setInterval(getTypes, delay);
} else {
console.error("Max retry attempts reached. Stopping retries.");
}
}
}
// 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) {
reactFlowInstance.setNodes(
reactFlowInstance.getNodes().filter((n: Node) => n.id !== idx)
);
reactFlowInstance.setEdges(
reactFlowInstance
.getEdges()
.filter((ns) => ns.source !== idx && ns.target !== idx)
);
}
return (
<typesContext.Provider
value={{
types,
setTypes,
reactFlowInstance,
setReactFlowInstance,
deleteNode,
setTemplates,
templates,
data,
setData,
}}
>
{children}
</typesContext.Provider>
);
}

View file

@ -1,55 +1,78 @@
import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { classNames } from "../../../utils";
import { useRef } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import { TabsContext } from "../../../contexts/tabsContext";
export default function ChatInput({
lockChat,
chatValue,
sendMessage,
setChatValue,
}: {
lockChat: boolean;
chatValue: string;
sendMessage: Function;
setChatValue: Function;
lockChat,
chatValue,
sendMessage,
setChatValue,
inputRef,
}) {
const inputRef = useRef(null);
return (
<>
<textarea
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage();
}
}}
ref={inputRef}
disabled={lockChat}
style={{ resize: "none" }}
value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => {
setChatValue(e.target.value);
}}
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",
"form-input block w-full custom-scroll h-10 rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm"
)}
placeholder={"Send a message..."}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<LockClosedIcon
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true"
/>
) : (
<PaperAirplaneIcon
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true"
/>
)}
</button>
</div>
</>
);
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 (
<div className="relative">
<textarea
onKeyDown={(event) => {
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
sendMessage();
}
}}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
rows={1}
ref={inputRef}
disabled={lockChat}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : chatValue}
onChange={(e) => {
setChatValue(e.target.value);
}}
className={classNames(
lockChat
? " 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..."}
/>
<div className="absolute bottom-0.5 right-3">
<button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? (
<LockClosedIcon
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true"
/>
) : (
<PaperAirplaneIcon
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true"
/>
)}
</button>
</div>
</div>
);
}

View file

@ -1,5 +1,8 @@
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 { FlowType, NodeType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext";
@ -37,8 +40,7 @@ export default function ChatModal({
}, [open]);
useEffect(() => {
id.current = flow.id;
},[flow.id])
}, [flow.id]);
var isStream = false;
@ -122,16 +124,16 @@ export default function ChatModal({
newChatHistory.push(
chatItem.files
? {
isSend: !chatItem.is_bot,
message: chatItem.message,
thought: chatItem.intermediate_steps,
files: chatItem.files,
}
isSend: !chatItem.is_bot,
message: chatItem.message,
thought: chatItem.intermediate_steps,
files: chatItem.files,
}
: {
isSend: !chatItem.is_bot,
message: chatItem.message,
thought: chatItem.intermediate_steps,
}
isSend: !chatItem.is_bot,
message: chatItem.message,
thought: chatItem.intermediate_steps,
}
);
}
}
@ -144,6 +146,9 @@ export default function ChatModal({
isStream = true;
}
if (data.type === "end") {
if (data.message) {
updateLastMessage({ str: data.message, end: true });
}
if (data.intermediate_steps) {
updateLastMessage({
str: data.message,
@ -171,8 +176,9 @@ export default function ChatModal({
const urlWs =
process.env.NODE_ENV === "development"
? `ws://localhost:7860/chat/${id.current}`
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host
}/chat/${id.current}`;
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${
window.location.host
}/chat/${id.current}`;
const newWs = new WebSocket(urlWs);
newWs.onopen = () => {
console.log("WebSocket connection established!");
@ -189,10 +195,9 @@ export default function ChatModal({
};
newWs.onerror = (ev) => {
console.log(ev, "error");
if(flow.id===""){
if (flow.id === "") {
connectWS();
}
else{
} else {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
@ -205,10 +210,9 @@ export default function ChatModal({
};
ws.current = newWs;
} catch {
if(flow.id===""){
if (flow.id === "") {
connectWS();
}
else{
} else {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
@ -279,11 +283,12 @@ export default function ChatModal({
e.targetHandle.split("|")[2] === n.id
)
? [
`${type} is missing ${template.display_name
? template.display_name
: toNormalCase(template[t].name)
}.`,
]
`${type} is missing ${
template.display_name
? template.display_name
: toNormalCase(template[t].name)
}.`,
]
: []
),
[] as string[]
@ -298,6 +303,12 @@ export default function ChatModal({
const ref = useRef(null);
useEffect(() => {
if (open && ref.current) {
ref.current.focus();
}
}, [open]);
function sendMessage() {
if (chatValue !== "") {
let nodeValidationErrors = validateNodes();
@ -382,7 +393,9 @@ export default function ChatModal({
</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">
{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">
<span>
@ -406,12 +419,13 @@ export default function ChatModal({
<div ref={ref}></div>
</div>
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center justify-between p-3">
<div className="relative w-full mt-1 rounded-md shadow-sm">
<div className="relative w-full mt-1 rounded-md shadow-sm">
<ChatInput
chatValue={chatValue}
lockChat={lockChat}
sendMessage={sendMessage}
setChatValue={setChatValue}
inputRef={ref}
/>
</div>
</div>

View file

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

View file

@ -16,7 +16,7 @@ export default function ExportModal() {
const { closePopUp } = useContext(PopUpContext);
const ref = useRef();
const { setErrorData } = useContext(alertContext);
const { flows, tabIndex, updateFlow, downloadFlow,setDisableCP } = useContext(TabsContext);
const { flows, tabIndex, updateFlow, downloadFlow,setDisableCopyPaste } = useContext(TabsContext);
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
@ -114,10 +114,10 @@ export default function ExportModal() {
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"
onBlur={() => {
setDisableCP(false);
setDisableCopyPaste(false);
}}
onFocus={() => {
setDisableCP(true);
setDisableCopyPaste(true);
}}
/>
</div>
@ -134,10 +134,10 @@ export default function ExportModal() {
</label>
<textarea
onBlur={() => {
setDisableCP(false);
setDisableCopyPaste(false);
}}
onFocus={() => {
setDisableCP(true);
setDisableCopyPaste(true);
}}
name="description"
id="description"
@ -164,10 +164,10 @@ export default function ExportModal() {
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"
onBlur={() => {
setDisableCP(false);
setDisableCopyPaste(false);
}}
onFocus={() => {
setDisableCP(true);
setDisableCopyPaste(true);
}}
/>
<span className="ml-2 font-medium text-gray-700 dark:text-white">

View file

@ -1,109 +1,153 @@
import React, { ReactNode } from "react";
import React, { ReactNode, useEffect } from "react";
import { DocumentDuplicateIcon } from "@heroicons/react/solid";
import { classNames } from "../../../utils";
import Tooltip from "../../../components/TooltipComponent";
export default function ButtonBox({
onClick,
title,
description,
icon,
bgColor,
textColor,
deactivate,
size,
onClick,
title,
description,
icon,
bgColor,
textColor,
deactivate,
size,
}: {
onClick: () => void;
title: string;
description: string;
icon: ReactNode;
bgColor: string;
textColor: string;
deactivate?: boolean;
size: "small" | "medium" | "big";
onClick: () => void;
title: string;
description: string;
icon: ReactNode;
bgColor: string;
textColor: string;
deactivate?: boolean;
size: "small" | "medium" | "big";
}) {
let bigCircle: string;
let smallCircle: string;
let titleFontSize: string;
let descriptionFontSize: string;
let padding: string;
let marginTop: string;
let height: string;
let width: string;
switch (size) {
case "small":
bigCircle = "h-12 w-12";
smallCircle = "h-8 w-8";
titleFontSize = "text-sm";
descriptionFontSize = "text-xs";
padding = "p-2 py-3";
marginTop = "mt-2";
height = "h-36";
width = "w-32";
break;
case "medium":
bigCircle = "h-16 w-16";
smallCircle = "h-12 w-12";
titleFontSize = "text-base";
descriptionFontSize = "text-sm";
padding = "p-4 py-5";
marginTop = "mt-3";
height = "h-44";
width = "w-36";
break;
case "big":
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
default:
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
}
return (
<button disabled={deactivate} onClick={onClick}>
<div
className={classNames(
"flex flex-col justify-center items-center rounded-lg text-center shadow border border-gray-300 dark:border-gray-800 hover:shadow-lg transform hover:scale-105",
bgColor,
height,
width,
padding
)}
>
<div
className={`flex items-center justify-center ${bigCircle} bg-white/30 dark:bg-white/30 rounded-full`}
>
<div
className={`flex items-center justify-center ${smallCircle} bg-white dark:bg-white/80 rounded-full`}
>
<div className={textColor}>{icon}</div>
</div>
</div>
<div className="w-full mt-auto mb-auto">
<h3
className={classNames(
"w-full font-semibold break-words text-white dark:text-white/80",
titleFontSize,
marginTop
)}
>
{title}
</h3>
</div>
</div>
</button>
);
let bigCircle: string;
let smallCircle: string;
let titleFontSize: string;
let descriptionFontSize: string;
let padding: string;
let marginTop: string;
let height: string;
let width: string;
let textHeight: number;
let textWidth: number;
switch (size) {
case "small":
bigCircle = "h-12 w-12";
smallCircle = "h-8 w-8";
titleFontSize = "text-sm";
descriptionFontSize = "text-xs";
padding = "p-2 py-3";
marginTop = "mt-2";
height = "h-36";
textHeight = 70;
textWidth = 80;
width = "w-32";
break;
case "medium":
bigCircle = "h-16 w-16";
smallCircle = "h-12 w-12";
titleFontSize = "text-base";
descriptionFontSize = "text-sm";
padding = "p-4 py-5";
marginTop = "mt-3";
textHeight = 112;
textWidth = 162;
height = "h-44";
width = "w-36";
break;
case "big":
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
default:
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
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 (
<button disabled={deactivate} onClick={onClick}>
<div
className={classNames(
"flex flex-col justify-center items-center rounded-lg text-center shadow border border-gray-300 dark:border-gray-800 hover:shadow-lg transform hover:scale-105",
bgColor,
height,
width,
padding
)}
>
<div
className={`flex items-center justify-center ${bigCircle} bg-white/30 dark:bg-white/30 rounded-full`}
>
<div
className={`flex items-center justify-center ${smallCircle} bg-white dark:bg-white/80 rounded-full`}
>
<div className={textColor}>{icon}</div>
</div>
</div>
<h3
ref={titleRef}
className={classNames(
" font-semibold text-white dark:text-white/80",
marginTop
)}
>
{title}
</h3>
</div>
</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">
<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}
</span>
<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;
onClick: () => void;
}) {
const { removeFlow, updateFlow, flows, setDisableCP } = useContext(TabsContext);
const { removeFlow, updateFlow, flows, setDisableCopyPaste } = useContext(TabsContext);
const [isRename, setIsRename] = useState(false);
const [value, setValue] = useState("");
return (
@ -41,14 +41,14 @@ export default function TabComponent({
{isRename ? (
<input
onFocus={() => {
setDisableCP(true);
setDisableCopyPaste(true);
}}
autoFocus
className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28"
onBlur={() => {
setIsRename(false);
setDisableCopyPaste(false);
if (value !== "") {
setDisableCP(false);
let newFlow = _.cloneDeep(flow);
newFlow.name = value;
updateFlow(newFlow);

View file

@ -1,21 +1,23 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import ReactFlow, {
Background,
Controls,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
updateEdge,
EdgeChange,
Connection,
Edge,
useKeyPress,
useOnSelectionChange,
NodeDragHandler,
OnEdgesDelete,
OnNodesDelete,
SelectionDragHandler,
Background,
Controls,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
updateEdge,
EdgeChange,
Connection,
Edge,
useKeyPress,
NodeDragHandler,
OnEdgesDelete,
OnNodesDelete,
SelectionDragHandler,
useOnViewportChange,
OnSelectionChangeParams,
OnNodesChange,
} from "reactflow";
import _ from "lodash";
import { locationContext } from "../../contexts/locationContext";
@ -32,308 +34,318 @@ import { isValidConnection } from "../../utils";
import useUndoRedo from "./hooks/useUndoRedo";
const nodeTypes = {
genericNode: GenericNode,
genericNode: GenericNode,
};
export default function FlowPage({ flow }: { flow: FlowType }) {
let { updateFlow, incrementNodeId, disableCP} =
useContext(TabsContext);
const { types, reactFlowInstance, setReactFlowInstance, templates } =
useContext(typesContext);
const reactFlowWrapper = useRef(null);
let { updateFlow, disableCopyPaste, addFlow, getNodeId, paste } =
useContext(TabsContext);
const { types, reactFlowInstance, setReactFlowInstance, templates } =
useContext(typesContext);
const reactFlowWrapper = useRef(null);
const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo();
const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo();
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if ((event.ctrlKey || event.metaKey) && (event.key === 'c') && lastSelection && !disableCP) {
event.preventDefault();
setLastCopiedSelection(lastSelection);
}
if ((event.ctrlKey || event.metaKey) && (event.key === 'v') && lastCopiedSelection && !disableCP) {
event.preventDefault();
paste();
}
}
const [position, setPosition] = useState({ x: 0, y: 0 });
const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams>(null);
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 handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
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) => {
setPosition({ x: event.clientX, y: event.clientY });
};
useOnSelectionChange({
onChange: (flow) => { setLastSelection(flow); },
})
document.addEventListener("keydown", onKeyDown);
document.addEventListener("mousemove", handleMouseMove);
let paste = () => {
let minimumX = Infinity;
let minimumY = Infinity;
let idsMap = {};
lastCopiedSelection.nodes.forEach((n) => {
if (n.position.y < minimumY) {
minimumY = n.position.y
}
if (n.position.x < minimumX) {
minimumX = n.position.x;
}
});
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousemove", handleMouseMove);
};
}, [position, lastCopiedSelection, lastSelection]);
const bounds = reactFlowWrapper.current.getBoundingClientRect();
const insidePosition = reactFlowInstance.project({
x: position.x - bounds.left,
y: position.y - bounds.top
});
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
lastCopiedSelection.nodes.forEach((n) => {
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
const { setErrorData } = useContext(alertContext);
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true);
useEffect(() => {
if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, edges]);
//update flow when tabs change
useEffect(() => {
setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
// Generate a unique node ID
let newId = getId();
idsMap[n.id] = newId;
const onEdgesChangeMod = useCallback(
(s: EdgeChange[]) => {
onEdgesChange(s);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[onEdgesChange, setNodes]
);
// 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,
},
};
const onConnect = useCallback(
(params: Connection) => {
takeSnapshot();
setEdges((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) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[setEdges, setNodes, takeSnapshot]
);
// Add the new node to the list of nodes in state
setNodes((nds) => nds.map((e) => ({ ...e, selected: false })).concat({ ...newNode, selected: false }));
})
const onNodeDragStart: NodeDragHandler = useCallback(() => {
// 👇 make dragging a node undoable
takeSnapshot();
// 👉 you can place your event handlers here
}, [takeSnapshot]);
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 onSelectionDragStart: SelectionDragHandler = useCallback(() => {
// 👇 make dragging a selection undoable
takeSnapshot();
}, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
const { setErrorData } = useContext(alertContext);
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
function getId() {
return `dndnode_` + incrementNodeId();
}
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
takeSnapshot();
useEffect(() => {
if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, edges]);
//update flow when tabs change
useEffect(() => {
setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
// Get the current bounds of the ReactFlow wrapper element
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
const onEdgesChangeMod = useCallback(
(s: EdgeChange[]) => {
onEdgesChange(s);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[onEdgesChange, setNodes]
);
// Extract the data from the drag event and parse it as a JSON object
let data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("json")
);
const onConnect = useCallback(
(params: Connection) => {
takeSnapshot();
setEdges((eds) =>
addEdge({ ...params, className: "animate-pulse" }, eds)
);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[setEdges, setNodes, takeSnapshot]
);
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
// Calculate the position where the node should be created
const position = reactFlowInstance.project({
x: event.clientX - reactflowBounds.left,
y: event.clientY - reactflowBounds.top,
});
const onNodeDragStart: NodeDragHandler = useCallback(() => {
// 👇 make dragging a node undoable
takeSnapshot();
// 👉 you can place your event handlers here
}, [takeSnapshot]);
// Generate a unique node ID
let newId = getNodeId();
let newNode: NodeType;
const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
// 👇 make dragging a selection undoable
takeSnapshot();
}, [takeSnapshot]);
if (data.type !== "groupNode") {
// Create a new node object
newNode = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
} else {
// Create a new node object
newNode = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
// Add the new node to the list of nodes in state
}
setNodes((nds) => nds.concat(newNode));
},
// Specify dependencies for useCallback
[getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const onDelete = useCallback(
(mynodes) => {
takeSnapshot();
setEdges(
edges.filter(
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
)
);
},
[takeSnapshot, edges, setEdges]
);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
takeSnapshot();
const onEdgeUpdate = useCallback(
(oldEdge: Edge, newConnection: Connection) => {
if (isValidConnection(newConnection, reactFlowInstance)) {
edgeUpdateSuccessful.current = true;
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[]
);
// Get the current bounds of the ReactFlow wrapper element
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
const onEdgeUpdateEnd = useCallback((_, edge) => {
if (!edgeUpdateSuccessful.current) {
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
}
// Extract the data from the drag event and parse it as a JSON object
let data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("json")
);
edgeUpdateSuccessful.current = true;
}, []);
// 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
const position = reactFlowInstance.project({
x: event.clientX - reactflowBounds.left,
y: event.clientY - reactflowBounds.top,
});
const [selectionEnded, setSelectionEnded] = useState(false);
// Generate a unique node ID
let newId = getId();
const onSelectionEnd = useCallback(() => {
setSelectionEnded(true);
}, []);
const onSelectionStart = useCallback(() => {
setSelectionEnded(false);
}, []);
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
// 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]);
// 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."],
});
}
},
// Specify dependencies for useCallback
[incrementNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
);
const onSelectionChange = useCallback((flow) => {
setLastSelection(flow);
}, []);
const onDelete = useCallback((mynodes) => {
takeSnapshot();
setEdges(
edges.filter(
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
)
);
}, [takeSnapshot, edges, setEdges]);
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
const onEdgeUpdate = useCallback(
(oldEdge: Edge, newConnection: Connection) => {
if (isValidConnection(newConnection, reactFlowInstance)) {
edgeUpdateSuccessful.current = true;
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[]
);
const onEdgeUpdateEnd = useCallback((_, edge) => {
if (!edgeUpdateSuccessful.current) {
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
}
edgeUpdateSuccessful.current = true;
}, []);
return (
<div className="w-full h-full" onMouseMove={handleMouseMove} ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
<>
<ReactFlow
nodes={nodes}
onMove={() =>
updateFlow({ ...flow, data: reactFlowInstance.toObject() })
}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChangeMod}
onKeyDown={(e) => onKeyDown(e)}
onConnect={onConnect}
onLoad={setReactFlowInstance}
onInit={setReactFlowInstance}
nodeTypes={nodeTypes}
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}
onNodesDelete={onDelete}
selectNodesOnDrag={false}
className="theme-attribution"
>
<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>
</ReactFlow>
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
</>
) : (
<></>
)}
</div>
);
return (
<div className="w-full h-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
<>
<ReactFlow
nodes={nodes}
onMove={() => {
updateFlow({ ...flow, data: reactFlowInstance.toObject() });
}}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChangeMod}
onConnect={onConnect}
onLoad={setReactFlowInstance}
onInit={setReactFlowInstance}
nodeTypes={nodeTypes}
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}
onNodesDelete={onDelete}
onSelectionChange={onSelectionChange}
selectNodesOnDrag={false}
>
<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>
</ReactFlow>
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
</>
) : (
<></>
)}
</div>
);
}

View file

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

View file

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

View file

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