diff --git a/src/backend/langflow/api/validate.py b/src/backend/langflow/api/validate.py
index a60bcc506..e1b5a3a1a 100644
--- a/src/backend/langflow/api/validate.py
+++ b/src/backend/langflow/api/validate.py
@@ -7,6 +7,7 @@ from langflow.api.base import (
PromptValidationResponse,
validate_prompt,
)
+from langflow.interface.run import build_graph
from langflow.utils.logger import logger
from langflow.utils.validate import validate_code
@@ -33,3 +34,20 @@ def post_validate_prompt(prompt: Prompt):
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+# validate node
+@router.post("/node/{node_id}", status_code=200)
+def post_validate_node(node_id: str, data: dict):
+ try:
+ # build graph
+ graph = build_graph(data)
+ # validate node
+ node = graph.get_node(node_id)
+ if node is not None:
+ _ = node.build()
+ return str(node.params)
+ raise Exception(f"Node {node_id} not found")
+ except Exception as e:
+ logger.exception(e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py
index c823ba531..bc275b665 100644
--- a/src/backend/langflow/interface/run.py
+++ b/src/backend/langflow/interface/run.py
@@ -39,16 +39,16 @@ def build_langchain_object_with_caching(data_graph):
"""
logger.debug("Building langchain object")
- nodes = data_graph["nodes"]
- # Add input variables
- # nodes = payload.extract_input_variables(nodes)
- # Nodes, edges and root node
- edges = data_graph["edges"]
- graph = Graph(nodes, edges)
-
+ graph = build_graph(data_graph)
return graph.build()
+def build_graph(data_graph):
+ nodes = data_graph["nodes"]
+ edges = data_graph["edges"]
+ return Graph(nodes, edges)
+
+
def build_langchain_object(data_graph):
"""
Build langchain object from data_graph.
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 9b90b435e..e24e28bd9 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -40,6 +40,7 @@
"reactflow": "^11.5.5",
"tailwindcss": "^3.2.6",
"typescript": "^4.9.5",
+ "use-debounce": "^9.0.4",
"web-vitals": "^2.1.4"
}
},
@@ -17044,6 +17045,17 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-debounce": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
+ "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
diff --git a/src/frontend/package.json b/src/frontend/package.json
index c291ea882..2086aa817 100644
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -35,6 +35,7 @@
"reactflow": "^11.5.5",
"tailwindcss": "^3.2.6",
"typescript": "^4.9.5",
+ "use-debounce": "^9.0.4",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx
index f362ca8ff..93497c0fc 100644
--- a/src/frontend/src/CustomNodes/GenericNode/index.tsx
+++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx
@@ -1,136 +1,203 @@
import { TrashIcon } from "@heroicons/react/24/outline";
+import { useDebouncedCallback } from "use-debounce";
import {
- classNames,
- nodeColors,
- nodeIcons,
- snakeToNormalCase,
+ classNames,
+ nodeColors,
+ nodeIcons,
+ snakeToNormalCase,
} from "../../utils";
import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext";
-import { useContext, useRef } from "react";
+import { useContext, useState, useEffect, useRef } from "react";
import { NodeDataType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext";
+import { useCallback } from "react";
export default function GenericNode({
- data,
- selected,
+ data,
+ selected,
}: {
- data: NodeDataType;
- selected: boolean;
+ data: NodeDataType;
+ selected: boolean;
}) {
- const { setErrorData } = useContext(alertContext);
- const showError = useRef(true);
- const { types, deleteNode } = useContext(typesContext);
- const Icon = nodeIcons[types[data.type]];
- 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;
- }
- return (
-
-
-
-
-
+ const { setErrorData } = useContext(alertContext);
+ const showError = useRef(true);
+ const { types, deleteNode } = useContext(typesContext);
+ const Icon = nodeIcons[types[data.type]];
+ const [validationStatus, setValidationStatus] = useState("idle");
+ // State for outline color
+ const [isGreenOutline, setIsGreenOutline] = useState(false);
+ const [isRedOutline, setIsRedOutline] = useState(false);
+ const { reactFlowInstance } = useContext(typesContext);
-
-
- {data.node.description}
-
+ const debouncedValidateNode = useDebouncedCallback(async () => {
+ // Check if the validationStatus is "success"
+ if (validationStatus === "success") return;
- <>
- {Object.keys(data.node.template)
- .filter((t) => t.charAt(0) !== "_")
- .map((t: string, idx) => (
-
- {idx === 0 ? (
-
- !key.startsWith("_") && data.node.template[key].show
- ).length === 0
- ? "hidden"
- : ""
- )}
- >
- Inputs
-
- ) : (
- <>>
- )}
- {data.node.template[t].show ? (
-
- ) : (
- <>>
- )}
-
- ))}
-
- Output
-
-
- >
-
-
- );
+ 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) {
+ setValidationStatus("success");
+ } else if (response.status === 500) {
+ setValidationStatus("error");
+ }
+ } catch (error) {
+ console.error("Error validating node:", error);
+ setValidationStatus("error");
+ }
+ }, 1000);
+
+ const validateNode = useCallback(() => {
+ debouncedValidateNode();
+ }, [debouncedValidateNode]);
+
+ useEffect(() => {
+ validateNode();
+ }, [
+ validateNode,
+ ...Object.values(data.node.template).flatMap((t) => Object.values(t)),
+ ]);
+
+ useEffect(() => {
+ if (validationStatus === "success") {
+ setIsGreenOutline(true);
+ setIsRedOutline(false);
+ setTimeout(() => {
+ setIsGreenOutline(false);
+ }, 1000);
+ } else if (validationStatus === "error") {
+ setIsRedOutline(true);
+ setIsGreenOutline(false);
+ } else {
+ setIsGreenOutline(false);
+ setIsRedOutline(false);
+ }
+ }, [validationStatus]);
+
+ const outlineColor = isGreenOutline
+ ? "animate-pulse-green"
+ : isRedOutline
+ ? "border-red-outline"
+ : "";
+
+ 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;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {data.node.description}
+
+
+ <>
+ {Object.keys(data.node.template)
+ .filter((t) => t.charAt(0) !== "_")
+ .map((t: string, idx) => (
+
+ {idx === 0 ? (
+
+ !key.startsWith("_") && data.node.template[key].show
+ ).length === 0
+ ? "hidden"
+ : ""
+ )}
+ >
+ Inputs
+
+ ) : (
+ <>>
+ )}
+ {data.node.template[t].show ? (
+
+ ) : (
+ <>>
+ )}
+
+ ))}
+
+ Output
+
+
+ >
+
+
+ );
}
diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js
index 40d4a433d..c679a87bc 100644
--- a/src/frontend/tailwind.config.js
+++ b/src/frontend/tailwind.config.js
@@ -1,36 +1,55 @@
/** @type {import('tailwindcss').Config} */
-const plugin = require('tailwindcss/plugin')
+const plugin = require("tailwindcss/plugin");
module.exports = {
content: ["./src/**/*.{js,ts,tsx,jsx}"],
- darkMode: 'class',
- important:true,
+ darkMode: "class",
+ important: true,
theme: {
- extend: {},
+ extend: {
+ borderColor: {
+ "red-outline": "rgba(255, 0, 0, 0.8)",
+ "green-outline": "rgba(72, 187, 120, 0.7)",
+ },
+ boxShadow: {
+ "red-outline": "0 0 5px rgba(255, 0, 0, 0.5)",
+ "green-outline": "0 0 5px rgba(72, 187, 120, 0.7)",
+ },
+
+ animation: {
+ "pulse-green": "pulseGreen 1s linear",
+ },
+ keyframes: {
+ pulseGreen: {
+ "0%": { boxShadow: "0 0 0 0 rgba(72, 187, 120, 0.7)" },
+ "100%": { boxShadow: "0 0 0 10px rgba(72, 187, 120, 0)" },
+ },
+ },
+ },
},
plugins: [
require("@tailwindcss/forms")({
- strategy: 'class', // only generate classes
+ strategy: "class", // only generate classes
}),
plugin(function ({ addUtilities }) {
addUtilities({
- '.scrollbar-hide': {
+ ".scrollbar-hide": {
/* IE and Edge */
- '-ms-overflow-style': 'none',
+ "-ms-overflow-style": "none",
/* Firefox */
- 'scrollbar-width': 'none',
+ "scrollbar-width": "none",
/* Safari and Chrome */
- '&::-webkit-scrollbar': {
- display: 'none'
- }
- },
- '.arrow-hide':{
- '&::-webkit-inner-spin-button':{
- '-webkit-appearance': 'none',
- 'margin': 0
+ "&::-webkit-scrollbar": {
+ display: "none",
},
- '&::-webkit-outer-spin-button':{
- '-webkit-appearance': 'none',
- 'margin': 0
+ },
+ ".arrow-hide": {
+ "&::-webkit-inner-spin-button": {
+ "-webkit-appearance": "none",
+ margin: 0,
+ },
+ "&::-webkit-outer-spin-button": {
+ "-webkit-appearance": "none",
+ margin: 0,
},
},
'.password':{
@@ -56,4 +75,4 @@ module.exports = {
})
}),require('@tailwindcss/line-clamp')
],
-}
+};