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:
commit
9d910b769a
37 changed files with 2418 additions and 1018 deletions
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -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"}'
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -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)
|
||||
|
|
|
|||
93
README.md
93
README.md
|
|
@ -46,13 +46,98 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
|
|||
[](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
|
||||
|
||||
[](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
16
lcserve.Dockerfile
Normal 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
1229
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from importlib.metadata import version
|
||||
import logging
|
||||
from importlib.metadata import version
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
|
|
|
|||
|
|
@ -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)})
|
||||
|
|
|
|||
|
|
@ -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}()"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
2
src/backend/langflow/jcloud.yml
Normal file
2
src/backend/langflow/jcloud.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
instance: C4
|
||||
autoscale_min: 1
|
||||
16
src/backend/langflow/lcserve.py
Normal file
16
src/backend/langflow/lcserve.py
Normal 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",
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ module.exports = {
|
|||
},
|
||||
});
|
||||
}),
|
||||
require("@tailwindcss/line-clamp"),
|
||||
require("@tailwindcss/typography"),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue