Merge remote-tracking branch 'origin/zustand/io/migration' into globalVariables
This commit is contained in:
commit
0f867dc485
46 changed files with 1157 additions and 485 deletions
|
|
@ -10,19 +10,7 @@ from langflow.schema.schema import Record
|
|||
|
||||
# Types of files that can be read simply by file.read()
|
||||
# and have 100% to be completely readable
|
||||
TEXT_FILE_TYPES = [
|
||||
"txt",
|
||||
"md",
|
||||
"mdx",
|
||||
"csv",
|
||||
"json",
|
||||
"yaml",
|
||||
"yml",
|
||||
"xml",
|
||||
"html",
|
||||
"htm",
|
||||
"pdf",
|
||||
]
|
||||
TEXT_FILE_TYPES = ["txt", "md", "mdx", "csv", "json", "yaml", "yml", "xml", "html", "htm", "pdf", "docx"]
|
||||
|
||||
|
||||
def is_hidden(path: Path) -> bool:
|
||||
|
|
@ -84,6 +72,13 @@ def read_text_file(file_path: str) -> str:
|
|||
return f.read()
|
||||
|
||||
|
||||
def read_docx_file(file_path: str) -> str:
|
||||
from docx import Document # type: ignore
|
||||
|
||||
doc = Document(file_path)
|
||||
return "\n\n".join([p.text for p in doc.paragraphs])
|
||||
|
||||
|
||||
def parse_pdf_to_text(file_path: str) -> str:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
|
||||
|
|
@ -96,6 +91,8 @@ def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[R
|
|||
try:
|
||||
if file_path.endswith(".pdf"):
|
||||
text = parse_pdf_to_text(file_path)
|
||||
elif file_path.endswith(".docx"):
|
||||
text = read_docx_file(file_path)
|
||||
else:
|
||||
text = read_text_file(file_path)
|
||||
# if file is json, yaml, or xml, we can parse it
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record
|
||||
|
|
@ -6,12 +6,17 @@ from langflow.schema import Record
|
|||
|
||||
|
||||
class FileComponent(CustomComponent):
|
||||
display_name = "File"
|
||||
description = "Load a file."
|
||||
display_name = "Files"
|
||||
description = "Read Text Files"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {"display_name": "Path"},
|
||||
"paths": {
|
||||
"display_name": "Paths",
|
||||
"field_type": "file",
|
||||
"file_types": TEXT_FILE_TYPES,
|
||||
"info": f"Supported file types: {', '.join(TEXT_FILE_TYPES)}",
|
||||
},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
|
|
@ -19,13 +24,22 @@ class FileComponent(CustomComponent):
|
|||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
silent_errors: bool = False,
|
||||
) -> Optional[Record]:
|
||||
def load_file(self, path: str, silent_errors: bool = False) -> Record:
|
||||
resolved_path = self.resolve_path(path)
|
||||
extension = resolved_path.split(".")[-1]
|
||||
if extension == "doc":
|
||||
raise ValueError("doc files are not supported. Please save as .docx")
|
||||
if extension not in TEXT_FILE_TYPES:
|
||||
raise ValueError(f"Unsupported file type: {extension}")
|
||||
return parse_text_file_to_record(resolved_path, silent_errors)
|
||||
record = parse_text_file_to_record(resolved_path, silent_errors)
|
||||
self.status = record if record else "No data"
|
||||
return record or Record()
|
||||
|
||||
def build(
|
||||
self,
|
||||
paths: List[str],
|
||||
silent_errors: bool = False,
|
||||
) -> List[Record]:
|
||||
records = [self.load_file(path, silent_errors) for path in paths]
|
||||
self.status = records
|
||||
return records
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
from langchain_core.vectorstores import VectorStoreRetriever
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import VectorStore
|
||||
|
||||
|
||||
class VectoStoreRetrieverComponent(CustomComponent):
|
||||
display_name = "VectorStore Retriever"
|
||||
description = "A vector store retriever"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"vectorstore": {"display_name": "Vector Store", "type": VectorStore},
|
||||
}
|
||||
|
||||
def build(self, vectorstore: VectorStore) -> VectorStoreRetriever:
|
||||
return vectorstore.as_retriever()
|
||||
32
src/backend/langflow/components/tools/RetrieverTool.py
Normal file
32
src/backend/langflow/components/tools/RetrieverTool.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from langchain.tools.retriever import create_retriever_tool
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseRetriever, Tool
|
||||
|
||||
|
||||
class RetrieverToolComponent(CustomComponent):
|
||||
display_name = "RetrieverTool"
|
||||
description = "Tool for interacting with retriever"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"retriever": {
|
||||
"display_name": "Retriever",
|
||||
"info": "Retriever to interact with",
|
||||
"type": BaseRetriever,
|
||||
},
|
||||
"name": {"display_name": "Name", "info": "Name of the tool"},
|
||||
"description": {"display_name": "Description", "info": "Description of the tool"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
retriever: BaseRetriever,
|
||||
name: str,
|
||||
description: str,
|
||||
) -> Tool:
|
||||
return create_retriever_tool(
|
||||
retriever=retriever,
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
0
src/backend/langflow/components/tools/__init__.py
Normal file
0
src/backend/langflow/components/tools/__init__.py
Normal file
|
|
@ -36,13 +36,12 @@ class MongoDBAtlasSearchComponent(MongoDBAtlasComponent, LCVectorStoreComponent)
|
|||
mongodb_atlas_cluster_uri: str = "",
|
||||
search_kwargs: Optional[NestedDict] = None,
|
||||
) -> List[Record]:
|
||||
search_kwargs = search_kwargs or {}
|
||||
vector_store = super().build(
|
||||
connection_string=mongodb_atlas_cluster_uri,
|
||||
namespace=f"{db_name}.{collection_name}",
|
||||
embedding=embedding,
|
||||
collection_name=collection_name,
|
||||
db_name=db_name,
|
||||
index_name=index_name,
|
||||
mongodb_atlas_cluster_uri=mongodb_atlas_cluster_uri,
|
||||
search_kwargs=search_kwargs,
|
||||
)
|
||||
if not vector_store:
|
||||
raise ValueError("Failed to create MongoDB Atlas Vector Store")
|
||||
|
|
|
|||
|
|
@ -9,13 +9,9 @@ from jose import JWTError, jwt
|
|||
from sqlmodel import Session
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
from langflow.services.database.models.api_key.model import ApiKey
|
||||
from langflow.services.database.models.api_key.crud import check_key
|
||||
from langflow.services.database.models.user.crud import (
|
||||
get_user_by_id,
|
||||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
from langflow.services.database.models.api_key.model import ApiKey
|
||||
from langflow.services.database.models.user.crud import get_user_by_id, get_user_by_username, update_user_last_login_at
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
|
|
@ -107,13 +103,13 @@ async def get_current_user_by_jwt(
|
|||
if isinstance(token, Coroutine):
|
||||
token = await token
|
||||
|
||||
if settings_service.auth_settings.SECRET_KEY is None:
|
||||
if settings_service.auth_settings.SECRET_KEY.get_secret_value() is None:
|
||||
raise credentials_exception
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings_service.auth_settings.SECRET_KEY,
|
||||
settings_service.auth_settings.SECRET_KEY.get_secret_value(),
|
||||
algorithms=[settings_service.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
|
|
@ -183,7 +179,7 @@ def create_token(data: dict, expires_delta: timedelta):
|
|||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
settings_service.auth_settings.SECRET_KEY,
|
||||
settings_service.auth_settings.SECRET_KEY.get_secret_value(),
|
||||
algorithm=settings_service.auth_settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
|
@ -287,7 +283,7 @@ def create_refresh_token(refresh_token: str, db: Session = Depends(get_session))
|
|||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings_service.auth_settings.SECRET_KEY,
|
||||
settings_service.auth_settings.SECRET_KEY.get_secret_value(),
|
||||
algorithms=[settings_service.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
|
|
@ -326,7 +322,7 @@ def add_padding(s):
|
|||
|
||||
|
||||
def get_fernet(settings_service=Depends(get_settings_service)):
|
||||
SECRET_KEY = settings_service.auth_settings.SECRET_KEY
|
||||
SECRET_KEY = settings_service.auth_settings.SECRET_KEY.get_secret_value()
|
||||
# It's important that your secret key is 32 url-safe base64-encoded byte
|
||||
padded_secret_key = add_padding(SECRET_KEY)
|
||||
fernet = Fernet(padded_secret_key)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
from passlib.context import CryptContext
|
||||
from pydantic import Field, SecretStr, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from langflow.services.settings.constants import (
|
||||
DEFAULT_SUPERUSER,
|
||||
DEFAULT_SUPERUSER_PASSWORD,
|
||||
)
|
||||
from langflow.services.settings.utils import read_secret_from_file, write_secret_to_file
|
||||
from loguru import logger
|
||||
from passlib.context import CryptContext
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class AuthSettings(BaseSettings):
|
||||
# Login settings
|
||||
CONFIG_DIR: str
|
||||
SECRET_KEY: str = Field(
|
||||
default="",
|
||||
SECRET_KEY: SecretStr = Field(
|
||||
default=None,
|
||||
description="Secret key for JWT. If not provided, a random one will be generated.",
|
||||
frozen=False,
|
||||
)
|
||||
|
|
@ -26,7 +26,6 @@ class AuthSettings(BaseSettings):
|
|||
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 12 * 7
|
||||
|
||||
# API Key to execute /process endpoint
|
||||
API_KEY_SECRET_KEY: Optional[str] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a"
|
||||
API_KEY_ALGORITHM: str = "HS256"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import "reactflow/dist/style.css";
|
|||
import "./App.css";
|
||||
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ErrorAlert from "./alerts/error";
|
||||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
|
|
@ -47,6 +48,9 @@ export default function App() {
|
|||
(state) => state.setGlobalVariables
|
||||
);
|
||||
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
refreshStars();
|
||||
|
|
@ -67,11 +71,12 @@ export default function App() {
|
|||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
checkApplicationHealth();
|
||||
// Timer to call getHealth every 5 seconds
|
||||
const timer = setInterval(() => {
|
||||
getHealth()
|
||||
.then(() => {
|
||||
if (fetchError) setFetchError(false);
|
||||
onHealthCheck();
|
||||
})
|
||||
.catch(() => {
|
||||
setFetchError(true);
|
||||
|
|
@ -84,6 +89,30 @@ export default function App() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const checkApplicationHealth = () => {
|
||||
setIsLoadingHealth(true);
|
||||
getHealth()
|
||||
.then(() => {
|
||||
onHealthCheck();
|
||||
})
|
||||
.catch(() => {
|
||||
setFetchError(true);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoadingHealth(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const onHealthCheck = () => {
|
||||
setFetchError(false);
|
||||
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
|
||||
if (isLoading === true && window.location.pathname === "/") {
|
||||
navigate("/flows");
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<div className="flex h-full flex-col">
|
||||
|
|
@ -93,20 +122,29 @@ export default function App() {
|
|||
}}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
{fetchError ? (
|
||||
<FetchErrorComponent
|
||||
description={FETCH_ERROR_DESCRIPION}
|
||||
message={FETCH_ERROR_MESSAGE}
|
||||
></FetchErrorComponent>
|
||||
) : isLoading ? (
|
||||
<div className="loading-page-panel">
|
||||
<LoadingComponent remSize={50} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Router />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{
|
||||
<FetchErrorComponent
|
||||
description={FETCH_ERROR_DESCRIPION}
|
||||
message={FETCH_ERROR_MESSAGE}
|
||||
openModal={fetchError}
|
||||
setRetry={() => {
|
||||
checkApplicationHealth();
|
||||
}}
|
||||
isLoadingHealth={isLoadingHealth}
|
||||
></FetchErrorComponent>
|
||||
}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-page-panel">
|
||||
<LoadingComponent remSize={50} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Router />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
<div></div>
|
||||
<div className="app-div">
|
||||
|
|
|
|||
|
|
@ -275,7 +275,8 @@ export default function GenericNode({
|
|||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : validationStatus && !validationStatus.valid ? (
|
||||
) : buildStatus === BuildStatus.ERROR ||
|
||||
(validationStatus && !validationStatus.valid) ? (
|
||||
<Xmark
|
||||
isVisible={true}
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
|
||||
|
|
@ -301,7 +302,10 @@ export default function GenericNode({
|
|||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (buildStatus === BuildStatus.BUILT && isInvalid) {
|
||||
if (
|
||||
(buildStatus === BuildStatus.BUILT && isInvalid) ||
|
||||
buildStatus === BuildStatus.ERROR
|
||||
) {
|
||||
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "building-status";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Loading from "../../../components/ui/loading";
|
|||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
import { MISSED_ERROR_ALERT } from "../../../constants/alerts_constants";
|
||||
import { BuildStatus } from "../../../constants/enums";
|
||||
import useAlertStore from "../../../stores/alertStore";
|
||||
import useFlowStore from "../../../stores/flowStore";
|
||||
import { validateNodes } from "../../../utils/reactflowUtils";
|
||||
|
|
@ -22,6 +23,7 @@ export default function BuildTrigger({
|
|||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||
|
|
@ -32,12 +34,15 @@ export default function BuildTrigger({
|
|||
if (isBuilding) {
|
||||
return;
|
||||
}
|
||||
const errors = validateNodes(nodes, edges);
|
||||
const errorsObjs = validateNodes(nodes, edges);
|
||||
const errors = errorsObjs.flatMap((errorObj) => errorObj.errors);
|
||||
if (errors.length > 0) {
|
||||
setErrorData({
|
||||
title: MISSED_ERROR_ALERT,
|
||||
list: errors,
|
||||
});
|
||||
const ids = errorsObjs.map((errorObj) => errorObj.id);
|
||||
updateBuildStatus(ids, BuildStatus.ERROR);
|
||||
return;
|
||||
}
|
||||
const minimumLoadingTime = 200; // in milliseconds
|
||||
|
|
|
|||
|
|
@ -1,16 +1,51 @@
|
|||
import BaseModal from "../../modals/baseModal";
|
||||
import { fetchErrorComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export default function FetchErrorComponent({
|
||||
message,
|
||||
description,
|
||||
openModal,
|
||||
setRetry,
|
||||
isLoadingHealth,
|
||||
}: fetchErrorComponentType) {
|
||||
return (
|
||||
<div role="status" className="m-auto flex flex-col items-center">
|
||||
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent>
|
||||
<br></br>
|
||||
<span className="text-lg text-almost-medium-blue">{message}</span>
|
||||
<span className="text-lg text-almost-medium-blue">{description}</span>
|
||||
</div>
|
||||
<>
|
||||
<BaseModal size="small-h-full" open={openModal} type="modal">
|
||||
<BaseModal.Content>
|
||||
<div role="status" className="m-auto flex flex-col items-center">
|
||||
<IconComponent
|
||||
className={`h-16 w-16`}
|
||||
name="Unplug"
|
||||
></IconComponent>
|
||||
<br></br>
|
||||
<span className="text-lg text-almost-medium-blue">{message}</span>
|
||||
<span className="text-lg text-almost-medium-blue">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<div className="m-auto">
|
||||
<Button
|
||||
disabled={isLoadingHealth}
|
||||
onClick={() => {
|
||||
setRetry();
|
||||
}}
|
||||
>
|
||||
{isLoadingHealth ? (
|
||||
<div>
|
||||
<IconComponent name={"Loader2"} className={"animate-spin"} />
|
||||
</div>
|
||||
) : (
|
||||
"Retry"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default function PromptAreaComponent({
|
|||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<GenericModal
|
||||
id={id}
|
||||
field_name={field_name}
|
||||
readonly={readonly}
|
||||
type={TypeModal.PROMPT}
|
||||
value={value}
|
||||
|
|
|
|||
119
src/frontend/src/components/ui/dialog-with-no-close.tsx
Normal file
119
src/frontend/src/components/ui/dialog-with-no-close.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal {...props}>
|
||||
<div className="nopan nodelete nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
|
|
@ -12,4 +12,5 @@ export enum BuildStatus {
|
|||
TO_BUILD = "TO_BUILD",
|
||||
BUILT = "BUILT",
|
||||
INACTIVE = "INACTIVE",
|
||||
ERROR = "ERROR",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
|
||||
import {
|
||||
Dialog as Modal,
|
||||
DialogContent as ModalContent,
|
||||
} from "../../components/ui/dialog-with-no-close";
|
||||
|
||||
import { modalHeaderType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
|
|
@ -76,6 +82,7 @@ interface BaseModalProps {
|
|||
|
||||
disable?: boolean;
|
||||
onChangeOpenModal?: (open?: boolean) => void;
|
||||
type?: "modal" | "dialog";
|
||||
}
|
||||
function BaseModal({
|
||||
open,
|
||||
|
|
@ -83,6 +90,7 @@ function BaseModal({
|
|||
children,
|
||||
size = "large",
|
||||
onChangeOpenModal,
|
||||
type = "dialog",
|
||||
}: BaseModalProps) {
|
||||
const headerChild = React.Children.toArray(children).find(
|
||||
(child) => (child as React.ReactElement).type === Header
|
||||
|
|
@ -156,22 +164,43 @@ function BaseModal({
|
|||
|
||||
//UPDATE COLORS AND STYLE CLASSSES
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<DialogContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col ${height!} w-full transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<>
|
||||
{type === "modal" ? (
|
||||
<Modal open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<ModalContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col ${height!} w-full transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<DialogContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col ${height!} w-full transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
CHAT_SECOND_INITIAL_TEXT,
|
||||
LANGFLOW_CHAT_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
|
|
@ -49,6 +50,7 @@ export default function FormModal({
|
|||
}): JSX.Element {
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus);
|
||||
const flowState = useFlowStore((state) => state.flowState);
|
||||
const setFlowState = useFlowStore((state) => state.setFlowState);
|
||||
const [chatValue, setChatValue] = useState(() => {
|
||||
|
|
@ -388,7 +390,8 @@ export default function FormModal({
|
|||
|
||||
function sendMessage(): void {
|
||||
let nodeValidationErrors = validateNodes(nodes, edges);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
const errors = nodeValidationErrors.flatMap((error) => error.errors);
|
||||
if (errors.length === 0) {
|
||||
setLockChat(true);
|
||||
let inputs = flowState?.input_keys;
|
||||
setChatValue("");
|
||||
|
|
@ -412,8 +415,10 @@ export default function FormModal({
|
|||
} else {
|
||||
setErrorData({
|
||||
title: INFO_MISSING_ALERT,
|
||||
list: nodeValidationErrors,
|
||||
list: errors,
|
||||
});
|
||||
const ids = nodeValidationErrors.map((error) => error.id);
|
||||
updateBuildStatus(ids, BuildStatus.ERROR);
|
||||
}
|
||||
}
|
||||
function clearChat(): void {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,49 @@ export default function Page({
|
|||
const [lastSelection, setLastSelection] =
|
||||
useState<OnSelectionChangeParams | null>(null);
|
||||
|
||||
function handleGroupNode() {
|
||||
takeSnapshot();
|
||||
if (validateSelection(lastSelection!, edges).length === 0) {
|
||||
const clonedNodes = cloneDeep(nodes);
|
||||
const clonedEdges = cloneDeep(edges);
|
||||
const clonedSelection = cloneDeep(lastSelection);
|
||||
updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!);
|
||||
const { newFlow, removedEdges } = generateFlow(
|
||||
clonedSelection!,
|
||||
clonedNodes,
|
||||
clonedEdges,
|
||||
getRandomName()
|
||||
);
|
||||
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
|
||||
const newEdges = reconnectEdges(newGroupNode, removedEdges);
|
||||
setNodes([
|
||||
...clonedNodes.filter(
|
||||
(oldNodes) =>
|
||||
!clonedSelection?.nodes.some(
|
||||
(selectionNode) => selectionNode.id === oldNodes.id
|
||||
)
|
||||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges([
|
||||
...clonedEdges.filter(
|
||||
(oldEdge) =>
|
||||
!clonedSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
)
|
||||
),
|
||||
...newEdges,
|
||||
]);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: INVALID_SELECTION_ERROR_ALERT,
|
||||
list: validateSelection(lastSelection!, edges),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
@ -104,49 +147,7 @@ export default function Page({
|
|||
event.key === "g"
|
||||
) {
|
||||
event.preventDefault();
|
||||
takeSnapshot();
|
||||
if (validateSelection(lastSelection!, edges).length === 0) {
|
||||
const clonedNodes = cloneDeep(nodes);
|
||||
const clonedEdges = cloneDeep(edges);
|
||||
const clonedSelection = cloneDeep(lastSelection);
|
||||
updateIds(
|
||||
{ nodes: clonedNodes, edges: clonedEdges },
|
||||
clonedSelection!
|
||||
);
|
||||
const { newFlow, removedEdges } = generateFlow(
|
||||
clonedSelection!,
|
||||
clonedNodes,
|
||||
clonedEdges,
|
||||
getRandomName()
|
||||
);
|
||||
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
|
||||
const newEdges = reconnectEdges(newGroupNode, removedEdges);
|
||||
setNodes([
|
||||
...clonedNodes.filter(
|
||||
(oldNodes) =>
|
||||
!clonedSelection?.nodes.some(
|
||||
(selectionNode) => selectionNode.id === oldNodes.id
|
||||
)
|
||||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges([
|
||||
...clonedEdges.filter(
|
||||
(oldEdge) =>
|
||||
!clonedSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
)
|
||||
),
|
||||
...newEdges,
|
||||
]);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: INVALID_SELECTION_ERROR_ALERT,
|
||||
list: validateSelection(lastSelection!, edges),
|
||||
});
|
||||
}
|
||||
handleGroupNode();
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
|
|
@ -484,58 +485,7 @@ export default function Page({
|
|||
isVisible={selectionMenuVisible}
|
||||
nodes={lastSelection?.nodes}
|
||||
onClick={() => {
|
||||
takeSnapshot();
|
||||
if (
|
||||
validateSelection(lastSelection!, edges).length === 0
|
||||
) {
|
||||
const clonedNodes = cloneDeep(nodes);
|
||||
const clonedEdges = cloneDeep(edges);
|
||||
const clonedSelection = cloneDeep(lastSelection);
|
||||
updateIds(
|
||||
{ nodes: clonedNodes, edges: clonedEdges },
|
||||
clonedSelection!
|
||||
);
|
||||
const { newFlow, removedEdges } = generateFlow(
|
||||
clonedSelection!,
|
||||
clonedNodes,
|
||||
clonedEdges,
|
||||
getRandomName()
|
||||
);
|
||||
const newGroupNode = generateNodeFromFlow(
|
||||
newFlow,
|
||||
getNodeId
|
||||
);
|
||||
const newEdges = reconnectEdges(
|
||||
newGroupNode,
|
||||
removedEdges
|
||||
);
|
||||
setNodes([
|
||||
...clonedNodes.filter(
|
||||
(oldNodes) =>
|
||||
!clonedSelection?.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldNodes.id
|
||||
)
|
||||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges([
|
||||
...clonedEdges.filter(
|
||||
(oldEdge) =>
|
||||
!clonedSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
)
|
||||
),
|
||||
...newEdges,
|
||||
]);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: INVALID_SELECTION_ERROR_ALERT,
|
||||
list: validateSelection(lastSelection!, edges),
|
||||
});
|
||||
}
|
||||
handleGroupNode();
|
||||
}}
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
|
|
|||
|
|
@ -270,19 +270,10 @@ export default function NodeToolbarComponent({
|
|||
selected &&
|
||||
isGroup &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "u"
|
||||
event.key === "g"
|
||||
) {
|
||||
event.preventDefault();
|
||||
takeSnapshot();
|
||||
expandGroupNode(
|
||||
data.id,
|
||||
updateFlowPosition(position, data.node?.flow!),
|
||||
data.node!.template,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges
|
||||
);
|
||||
handleSelectChange("ungroup");
|
||||
}
|
||||
if (
|
||||
selected &&
|
||||
|
|
@ -571,7 +562,7 @@ export default function NodeToolbarComponent({
|
|||
Ctrl +{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.43em]">U</span>
|
||||
<span className="absolute right-2 top-[0.43em]">G</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -438,16 +438,20 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
const setNoticeData = useAlertStore.getState().setNoticeData;
|
||||
function validateSubgraph(nodes: string[]) {
|
||||
const errors = validateNodes(
|
||||
const errorsObjs = validateNodes(
|
||||
get().nodes.filter((node) => nodes.includes(node.id)),
|
||||
get().edges
|
||||
);
|
||||
const errors = errorsObjs.map((obj) => obj.errors).flat();
|
||||
if (errors.length > 0) {
|
||||
setErrorData({
|
||||
title: MISSED_ERROR_ALERT,
|
||||
list: errors,
|
||||
});
|
||||
get().setIsBuilding(false);
|
||||
const ids = errorsObjs.map((obj) => obj.id).flat();
|
||||
console.log("ids", ids);
|
||||
get().updateBuildStatus(ids, BuildStatus.ERROR);
|
||||
throw new Error("Invalid nodes");
|
||||
}
|
||||
}
|
||||
|
|
@ -538,7 +542,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
.filter(Boolean) as string[];
|
||||
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
|
||||
},
|
||||
validateNodes: validateSubgraph,
|
||||
onValidateNodes: validateSubgraph,
|
||||
});
|
||||
get().setIsBuilding(false);
|
||||
get().revertBuiltStatusFromBuilding();
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
set({ isLoading: false });
|
||||
useAlertStore.getState().setErrorData({
|
||||
title: "Could not load flows from database",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@ export const useTypesStore = create<TypesStoreType>((set, get) => ({
|
|||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
useAlertStore.getState().setErrorData({
|
||||
title: "An error has occurred while fetching types.",
|
||||
list: ["Please refresh the page."],
|
||||
});
|
||||
console.error("An error has occurred while fetching types.");
|
||||
console.log(error);
|
||||
reject();
|
||||
|
|
|
|||
|
|
@ -672,6 +672,9 @@ export type ApiKey = {
|
|||
export type fetchErrorComponentType = {
|
||||
message: string;
|
||||
description: string;
|
||||
openModal?: boolean;
|
||||
setRetry: () => void;
|
||||
isLoadingHealth: boolean;
|
||||
};
|
||||
|
||||
export type dropdownButtonPropsType = {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type BuildVerticesParams = {
|
|||
onBuildComplete?: (allNodesValid: boolean) => void;
|
||||
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
|
||||
onBuildStart?: (idList: VertexLayerElementType[]) => void;
|
||||
validateNodes?: (nodes: string[]) => void;
|
||||
onValidateNodes?: (nodes: string[]) => void;
|
||||
};
|
||||
|
||||
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
|
||||
|
|
@ -114,7 +114,7 @@ export async function buildVertices({
|
|||
onBuildComplete,
|
||||
onBuildError,
|
||||
onBuildStart,
|
||||
validateNodes,
|
||||
onValidateNodes,
|
||||
}: BuildVerticesParams) {
|
||||
let verticesBuild = useFlowStore.getState().verticesBuild;
|
||||
// if startNodeId and stopNodeId are provided
|
||||
|
|
@ -133,10 +133,10 @@ export async function buildVertices({
|
|||
|
||||
if (onGetOrderSuccess) onGetOrderSuccess();
|
||||
|
||||
if (validateNodes) {
|
||||
if (onValidateNodes) {
|
||||
try {
|
||||
const nodes = useFlowStore.getState().nodes;
|
||||
validateNodes(nodes.map((node) => node.id));
|
||||
onValidateNodes(nodes.map((node) => node.id));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,27 +313,40 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
|||
) {
|
||||
if (hasDuplicateKeys(template[t].value))
|
||||
errors.push(
|
||||
`${type} (${getFieldTitle(
|
||||
`${displayName || type} (${getFieldTitle(
|
||||
template,
|
||||
t
|
||||
)}) contains duplicate keys with the same values.`
|
||||
);
|
||||
if (hasEmptyKey(template[t].value))
|
||||
errors.push(
|
||||
`${type} (${getFieldTitle(template, t)}) field must not be empty.`
|
||||
`${displayName || type} (${getFieldTitle(
|
||||
template,
|
||||
t
|
||||
)}) field must not be empty.`
|
||||
);
|
||||
}
|
||||
return errors;
|
||||
}, [] as string[]);
|
||||
}
|
||||
|
||||
export function validateNodes(nodes: Node[], edges: Edge[]) {
|
||||
export function validateNodes(
|
||||
nodes: Node[],
|
||||
edges: Edge[]
|
||||
): // this returns an array of tuples with the node id and the errors
|
||||
Array<{ id: string; errors: Array<string> }> {
|
||||
if (nodes.length === 0) {
|
||||
return [
|
||||
"No nodes found in the flow. Please add at least one node to the flow.",
|
||||
{
|
||||
id: "",
|
||||
errors: [
|
||||
"No nodes found in the flow. Please add at least one node to the flow.",
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
return nodes.flatMap((n: NodeType) => validateNode(n, edges));
|
||||
// validateNode(n, edges) returns an array of errors for the node
|
||||
return nodes.map((n) => ({ id: n.id, errors: validateNode(n, edges) }));
|
||||
}
|
||||
|
||||
export function updateEdges(edges: Edge[]) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue