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 ( -
-
-
- -
{data.type}
-
- -
+ 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.type}
+
+ +
+ +
+
+ {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') ], -} +};