diff --git a/src/frontend/src/CustomNodes/genericNode/components/HandleTooltipComponent/index.tsx b/src/frontend/src/CustomNodes/genericNode/components/HandleTooltipComponent/index.tsx
new file mode 100644
index 000000000..2dddabbb5
--- /dev/null
+++ b/src/frontend/src/CustomNodes/genericNode/components/HandleTooltipComponent/index.tsx
@@ -0,0 +1,31 @@
+import { useRef } from "react";
+import { TOOLTIP_EMPTY } from "../../../../constants/constants";
+import { groupByFamily } from "../../../../utils/utils";
+import TooltipRenderComponent from "../tooltipRenderComponent";
+import { useTypesStore } from "../../../../stores/typesStore";
+import { NodeType } from "../../../../types/flow";
+import useFlowStore from "../../../../stores/flowStore";
+
+export default function HandleTooltips({
+ left,
+ tooltipTitle,
+}: {
+ left: boolean;
+ nodes: NodeType[];
+ tooltipTitle: string;
+}) {
+ const myData = useTypesStore((state) => state.data);
+ const nodes = useFlowStore((state) => state.nodes);
+
+ let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, nodes!);
+
+ if (groupedObj && groupedObj.length > 0) {
+ //@ts-ignore
+ return groupedObj.map((item, index) => {
+ return ;
+ });
+ } else {
+ //@ts-ignore
+ return {TOOLTIP_EMPTY} ;
+ }
+}
diff --git a/src/frontend/src/CustomNodes/genericNode/components/OutputComponent/index.tsx b/src/frontend/src/CustomNodes/genericNode/components/OutputComponent/index.tsx
new file mode 100644
index 000000000..f684a902f
--- /dev/null
+++ b/src/frontend/src/CustomNodes/genericNode/components/OutputComponent/index.tsx
@@ -0,0 +1,70 @@
+import { cloneDeep } from "lodash";
+import { useUpdateNodeInternals } from "reactflow";
+import ForwardedIconComponent from "../../../../components/genericIconComponent";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "../../../../components/ui/dropdown-menu";
+import useFlowStore from "../../../../stores/flowStore";
+import { outputComponentType } from "../../../../types/components";
+import { NodeDataType } from "../../../../types/flow";
+import { cn } from "../../../../utils/utils";
+import { Button } from "../../../../components/ui/button";
+
+export default function OutputComponent({
+ selected,
+ types,
+ frozen = false,
+ nodeId,
+ idx,
+ name,
+}: outputComponentType) {
+ const setNode = useFlowStore((state) => state.setNode);
+ const updateNodeInternals = useUpdateNodeInternals();
+
+ if (types.length < 2) {
+ return {selected} ;
+ }
+
+ return (
+
+
+
+
+ {selected}
+
+
+
+
+ {types.map((type) => (
+ {
+ // TODO: UDPDATE SET NODE TO NEW NODE FORM
+ setNode(nodeId, (node) => {
+ const newNode = cloneDeep(node);
+ (newNode.data as NodeDataType).node!.outputs![idx].selected =
+ type;
+ return newNode;
+ });
+ updateNodeInternals(nodeId);
+ }}
+ >
+ {type}
+
+ ))}
+
+
+ {name}
+
+ );
+}
diff --git a/src/frontend/src/CustomNodes/genericNode/components/parameterComponent/constants.ts b/src/frontend/src/CustomNodes/genericNode/components/parameterComponent/constants.ts
new file mode 100644
index 000000000..58cb45587
--- /dev/null
+++ b/src/frontend/src/CustomNodes/genericNode/components/parameterComponent/constants.ts
@@ -0,0 +1 @@
+export const TEXT_FIELD_TYPES: string[] = ["str", "SecretStr"];
diff --git a/src/frontend/src/CustomNodes/genericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/genericNode/components/parameterComponent/index.tsx
new file mode 100644
index 000000000..84e4587b1
--- /dev/null
+++ b/src/frontend/src/CustomNodes/genericNode/components/parameterComponent/index.tsx
@@ -0,0 +1,579 @@
+import { cloneDeep } from "lodash";
+import { ReactNode, useEffect, useRef, useState } from "react";
+import { Handle, Position, useUpdateNodeInternals } from "reactflow";
+import CodeAreaComponent from "../../../../components/codeAreaComponent";
+import DictComponent from "../../../../components/dictComponent";
+import Dropdown from "../../../../components/dropdownComponent";
+import FloatComponent from "../../../../components/floatComponent";
+import { default as IconComponent } from "../../../../components/genericIconComponent";
+import InputFileComponent from "../../../../components/inputFileComponent";
+import InputGlobalComponent from "../../../../components/inputGlobalComponent";
+import InputListComponent from "../../../../components/inputListComponent";
+import IntComponent from "../../../../components/intComponent";
+import KeypairListComponent from "../../../../components/keypairListComponent";
+import PromptAreaComponent from "../../../../components/promptComponent";
+import ShadTooltip from "../../../../components/shadTooltipComponent";
+import TextAreaComponent from "../../../../components/textAreaComponent";
+import ToggleShadComponent from "../../../../components/toggleShadComponent";
+import { Button } from "../../../../components/ui/button";
+import { RefreshButton } from "../../../../components/ui/refreshButton";
+import { LANGFLOW_SUPPORTED_TYPES } from "../../../../constants/constants";
+import { Case } from "../../../../shared/components/caseComponent";
+import useFlowStore from "../../../../stores/flowStore";
+import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
+import { useTypesStore } from "../../../../stores/typesStore";
+import { APIClassType } from "../../../../types/api";
+import { ParameterComponentType } from "../../../../types/components";
+import {
+ debouncedHandleUpdateValues,
+ handleUpdateValues,
+} from "../../../../utils/parameterUtils";
+import {
+ convertObjToArray,
+ convertValuesToNumbers,
+ hasDuplicateKeys,
+ isValidConnection,
+ scapedJSONStringfy,
+} from "../../../../utils/reactflowUtils";
+import { nodeColors } from "../../../../utils/styleUtils";
+import { classNames, groupByFamily } from "../../../../utils/utils";
+import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
+import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
+import useHandleNodeClass from "../../../hooks/use-handle-node-class";
+import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
+import HandleTooltips from "../HandleTooltipComponent";
+import OutputComponent from "../OutputComponent";
+import { TEXT_FIELD_TYPES } from "./constants";
+
+export default function ParameterComponent({
+ left,
+ id,
+ data,
+ tooltipTitle,
+ title,
+ color,
+ type,
+ name = "",
+ required = false,
+ optionalHandle = null,
+ info = "",
+ proxy,
+ showNode,
+ index,
+ outputName,
+}: ParameterComponentType): JSX.Element {
+ const infoHtml = useRef(null);
+ const nodes = useFlowStore((state) => state.nodes);
+ const edges = useFlowStore((state) => state.edges);
+ const setNode = useFlowStore((state) => state.setNode);
+ const myData = useTypesStore((state) => state.data);
+ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
+ const [isLoading, setIsLoading] = useState(false);
+ const updateNodeInternals = useUpdateNodeInternals();
+ const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
+ const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
+ const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
+ data,
+ name,
+ takeSnapshot,
+ handleUpdateValues,
+ debouncedHandleUpdateValues,
+ setNode,
+ isLoading,
+ setIsLoading
+ );
+
+ const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
+ data,
+ name,
+ takeSnapshot,
+ setNode,
+ updateNodeInternals
+ );
+
+ const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
+ useHandleRefreshButtonPress(setIsLoading, setNode);
+
+ let disabled =
+ edges.some(
+ (edge) =>
+ edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id)
+ ) ?? false;
+
+ const handleRefreshButtonPress = async (name, data) => {
+ handleRefreshButtonPressHook(name, data);
+ };
+
+ useFetchDataOnMount(data, name, handleUpdateValues, setNode, setIsLoading);
+
+ const handleOnNewValue = async (
+ newValue: string | string[] | boolean | Object[],
+ skipSnapshot: boolean | undefined = false
+ ): Promise => {
+ handleOnNewValueHook(newValue, skipSnapshot);
+ };
+
+ const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
+ handleNodeClassHook(newNodeClass, code);
+ };
+
+ useEffect(() => {
+ // @ts-ignore
+ infoHtml.current = (
+
+ {info.split("\n").map((line, index) => (
+
+ {line}
+
+ ))}
+
+ );
+ }, [info]);
+
+ function renderTitle() {
+ return !left ? (
+
+ ) : (
+ {title}
+ );
+ }
+
+ // If optionalHandle is an empty list, then it is not an optional handle
+ if (optionalHandle && optionalHandle.length === 0) {
+ optionalHandle = null;
+ }
+
+ return !showNode ? (
+ left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
+ <>>
+ ) : (
+
+
+
+ }
+ side={left ? "left" : "right"}
+ >
+
+ isValidConnection(connection, nodes, edges)
+ }
+ className={classNames(
+ left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
+ "h-3 w-3 rounded-full border-2 bg-background",
+ !showNode ? "mt-0" : ""
+ )}
+ style={{
+ borderColor: color ?? nodeColors.unknown,
+ }}
+ onClick={() => {
+ setFilterEdge(
+ groupByFamily(myData, tooltipTitle!, left, nodes!)
+ );
+ }}
+ >
+
+
+
+ )
+ ) : (
+
+ <>
+
+
+
+
+
+
+
+ {proxy ? (
+
{proxy.id}}>
+ {renderTitle()}
+
+ ) : (
+ renderTitle()
+ )}
+
+ {required ? "*" : ""}
+
+
+ {info !== "" && (
+
+ {/* put div to avoid bug that does not display tooltip */}
+
+
+
+
+ )}
+
+
+ {left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
+ <>>
+ ) : (
+
+
+
+ }
+ side={left ? "left" : "right"}
+ >
+
+ isValidConnection(connection, nodes, edges)
+ }
+ className={classNames(
+ left ? "-ml-0.5" : "-mr-0.5",
+ "h-3 w-3 rounded-full border-2 bg-background"
+ )}
+ style={{ borderColor: color ?? nodeColors.unknown }}
+ onClick={() => {
+ setFilterEdge(
+ groupByFamily(myData, tooltipTitle!, left, nodes!)
+ );
+ }}
+ />
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {data.node?.template[name]?.refresh_button && (
+
+
+
+ )}
+
+
+
+
+
+ {
+ setNode(data.id, (oldNode) => {
+ let newNode = cloneDeep(oldNode);
+ newNode.data = {
+ ...newNode.data,
+ };
+ newNode.data.node.template[name].load_from_db = value;
+ return newNode;
+ });
+ }}
+ name={name}
+ data={data.node?.template[name]}
+ />
+
+ {data.node?.template[name]?.refresh_button && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {data.node?.template[name]?.refresh_button && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ data.node!.template[name].file_path = filePath;
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const valueToNumbers = convertValuesToNumbers(newValue);
+ setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
+ // if data.node?.template[name]?.list is true, then the value is an array of objects
+ // else we need to get the first object of the array
+
+ if (data.node?.template[name]?.list) {
+ handleOnNewValue(valueToNumbers);
+ } else handleOnNewValue(valueToNumbers[0]);
+ }}
+ isList={data.node?.template[name]?.list ?? false}
+ />
+
+
+ >
+
+ );
+}
diff --git a/src/frontend/src/CustomNodes/genericNode/components/tooltipRenderComponent/index.tsx b/src/frontend/src/CustomNodes/genericNode/components/tooltipRenderComponent/index.tsx
new file mode 100644
index 000000000..c76bc7293
--- /dev/null
+++ b/src/frontend/src/CustomNodes/genericNode/components/tooltipRenderComponent/index.tsx
@@ -0,0 +1,91 @@
+import React from "react";
+import {
+ INPUT_HANDLER_HOVER,
+ OUTPUT_HANDLER_HOVER,
+} from "../../../../constants/constants";
+import {
+ nodeColors,
+ nodeIconsLucide,
+ nodeNames,
+} from "../../../../utils/styleUtils";
+import { classNames } from "../../../../utils/utils";
+
+const TooltipRenderComponent = ({ item, index, left }) => {
+ const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
+
+ return (
+
+ {index === 0 && (
+
{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}
+ )}
+
0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
+ )}
+ >
+
+
+
+
+ {nodeNames[item.family] ?? "Other"}{" "}
+ {item?.display_name && item?.display_name?.length > 0 ? (
+
+ {" "}
+ {item.display_name === "" ? "" : " - "}
+ {item.display_name.split(", ").length > 2
+ ? item.display_name.split(", ").map((el, index) => (
+
+
+ {index === item.display_name.split(", ").length - 1
+ ? el
+ : (el += `, `)}
+
+
+ ))
+ : item.display_name}
+
+ ) : (
+
+ {" "}
+ {item.type === "" ? "" : " - "}
+ {item.type.split(", ").length > 2
+ ? item.type.split(", ").map((el, index) => (
+
+
+ {index === item.type.split(", ").length - 1
+ ? el
+ : (el += `, `)}
+
+
+ ))
+ : item.type}
+
+ )}
+
+
+
+ );
+};
+
+export default TooltipRenderComponent;
diff --git a/src/frontend/src/CustomNodes/genericNode/index.tsx b/src/frontend/src/CustomNodes/genericNode/index.tsx
new file mode 100644
index 000000000..2e15ae6ec
--- /dev/null
+++ b/src/frontend/src/CustomNodes/genericNode/index.tsx
@@ -0,0 +1,860 @@
+import emojiRegex from "emoji-regex";
+import { cloneDeep } from "lodash";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
+import IconComponent from "../../components/genericIconComponent";
+import InputComponent from "../../components/inputComponent";
+import ShadTooltip from "../../components/shadTooltipComponent";
+import { Button } from "../../components/ui/button";
+import Checkmark from "../../components/ui/checkmark";
+import Loading from "../../components/ui/loading";
+import { Textarea } from "../../components/ui/textarea";
+import Xmark from "../../components/ui/xmark";
+import {
+ RUN_TIMESTAMP_PREFIX,
+ STATUS_BUILD,
+ STATUS_BUILDING,
+} from "../../constants/constants";
+import { BuildStatus } from "../../constants/enums";
+import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
+import useAlertStore from "../../stores/alertStore";
+import { useDarkStore } from "../../stores/darkStore";
+import useFlowStore from "../../stores/flowStore";
+import useFlowsManagerStore from "../../stores/flowsManagerStore";
+import { useTypesStore } from "../../stores/typesStore";
+import { APIClassType } from "../../types/api";
+import { validationStatusType } from "../../types/components";
+import { NodeDataType } from "../../types/flow";
+import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
+import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
+import { classNames, cn } from "../../utils/utils";
+import getFieldTitle from "../utils/get-field-title";
+import sortFields from "../utils/sort-fields";
+import ParameterComponent from "./components/parameterComponent";
+
+export default function GenericNode({
+ data,
+ xPos,
+ yPos,
+ selected,
+}: {
+ data: NodeDataType;
+ selected: boolean;
+ xPos: number;
+ yPos: number;
+}): JSX.Element {
+ const types = useTypesStore((state) => state.types);
+ const templates = useTypesStore((state) => state.templates);
+ const deleteNode = useFlowStore((state) => state.deleteNode);
+ const flowPool = useFlowStore((state) => state.flowPool);
+ const buildFlow = useFlowStore((state) => state.buildFlow);
+ const setNode = useFlowStore((state) => state.setNode);
+ const updateNodeInternals = useUpdateNodeInternals();
+ const setErrorData = useAlertStore((state) => state.setErrorData);
+ const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
+ const [inputName, setInputName] = useState(false);
+ const [nodeName, setNodeName] = useState(data.node!.display_name);
+ const [inputDescription, setInputDescription] = useState(false);
+ const [nodeDescription, setNodeDescription] = useState(
+ data.node?.description!,
+ );
+ const [isOutdated, setIsOutdated] = useState(false);
+ const buildStatus = useFlowStore(
+ (state) => state.flowBuildStatus[data.id]?.status,
+ );
+ const lastRunTime = useFlowStore(
+ (state) => state.flowBuildStatus[data.id]?.timestamp,
+ );
+ const [validationStatus, setValidationStatus] =
+ useState(null);
+ const [handles, setHandles] = useState(0);
+
+ const [validationString, setValidationString] = useState("");
+
+ const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
+
+ useEffect(() => {
+ // This one should run only once
+ // first check if data.type in NATIVE_CATEGORIES
+ // if not return
+ if (!data.node?.template?.code?.value) return;
+ const thisNodeTemplate = templates[data.type]?.template;
+ // if the template does not have a code key
+ // return
+ if (!thisNodeTemplate?.code) return;
+ const currentCode = thisNodeTemplate.code?.value;
+ const thisNodesCode = data.node!.template?.code?.value;
+ const componentsToIgnore = ["Custom Component"];
+ if (
+ currentCode !== thisNodesCode &&
+ !componentsToIgnore.includes(data.node!.display_name)
+ ) {
+ setIsOutdated(true);
+ } else {
+ setIsOutdated(false);
+ }
+ // template.code can be undefined
+ }, [data.node?.template?.code?.value]);
+
+ const updateNodeCode = useCallback(
+ (newNodeClass: APIClassType, code: string, name: string) => {
+ setNode(data.id, (oldNode) => {
+ let newNode = cloneDeep(oldNode);
+
+ newNode.data = {
+ ...newNode.data,
+ node: newNodeClass,
+ description: newNodeClass.description ?? data.node!.description,
+ display_name: newNodeClass.display_name ?? data.node!.display_name,
+ };
+
+ newNode.data.node.template[name].value = code;
+ setIsOutdated(false);
+
+ return newNode;
+ });
+
+ updateNodeInternals(data.id);
+ },
+ [data.id, data.node, setNode, setIsOutdated],
+ );
+
+ if (!data.node!.template) {
+ setErrorData({
+ title: `Error in component ${data.node!.display_name}`,
+ list: [
+ `The component ${data.node!.display_name} has no template.`,
+ `Please contact the developer of the component to fix this issue.`,
+ ],
+ });
+ takeSnapshot();
+ deleteNode(data.id);
+ }
+
+ function countHandles(): void {
+ let count = Object.keys(data.node!.template)
+ .filter((templateField) => templateField.charAt(0) !== "_")
+ .map((templateCamp) => {
+ const { template } = data.node!;
+ if (template[templateCamp].input_types) return true;
+ if (!template[templateCamp].show) return false;
+ switch (template[templateCamp].type) {
+ case "str":
+ case "bool":
+ case "float":
+ case "code":
+ case "prompt":
+ case "file":
+ case "int":
+ return false;
+ default:
+ return true;
+ }
+ })
+ .reduce((total, value) => total + (value ? 1 : 0), 0);
+
+ setHandles(count);
+ }
+ useEffect(() => {
+ countHandles();
+ }, [data, data.node]);
+
+ useEffect(() => {
+ if (!selected) {
+ setInputName(false);
+ setInputDescription(false);
+ }
+ }, [selected]);
+
+ // State for outline color
+ const isBuilding = useFlowStore((state) => state.isBuilding);
+
+ // should be empty string if no duration
+ // else should be `Duration: ${duration}`
+ const getDurationString = (duration: number | undefined): string => {
+ if (duration === undefined) {
+ return "";
+ } else {
+ return `${duration}`;
+ }
+ };
+ const durationString = getDurationString(validationStatus?.data.duration);
+
+ useEffect(() => {
+ setNodeDescription(data.node!.description);
+ }, [data.node!.description]);
+
+ useEffect(() => {
+ setNodeName(data.node!.display_name);
+ }, [data.node!.display_name]);
+
+ useEffect(() => {
+ const relevantData =
+ flowPool[data.id] && flowPool[data.id]?.length > 0
+ ? flowPool[data.id][flowPool[data.id].length - 1]
+ : null;
+ if (relevantData) {
+ // Extract validation information from relevantData and update the validationStatus state
+ setValidationStatus(relevantData);
+ } else {
+ setValidationStatus(null);
+ }
+ }, [flowPool[data.id], data.id]);
+
+ useEffect(() => {
+ if (validationStatus?.params) {
+ // if it is not a string turn it into a string
+ let newValidationString = validationStatus.params;
+ if (typeof newValidationString !== "string") {
+ newValidationString = JSON.stringify(validationStatus.params);
+ }
+
+ setValidationString(newValidationString);
+ }
+ }, [validationStatus, validationStatus?.params]);
+
+ const [showNode, setShowNode] = useState(data.showNode ?? true);
+
+ useEffect(() => {
+ setShowNode(data.showNode ?? true);
+ }, [data.showNode]);
+
+ const nameEditable = true;
+
+ const isEmoji = emojiRegex().test(data?.node?.icon!);
+
+ const iconNodeRender = useCallback(() => {
+ const iconElement = data?.node?.icon;
+ const iconColor = nodeColors[types[data.type]];
+ const iconName =
+ iconElement || (data.node?.flow ? "group_components" : name);
+ const iconClassName = `generic-node-icon ${
+ !showNode ? " absolute inset-x-6 h-12 w-12 " : ""
+ }`;
+ if (iconElement && isEmoji) {
+ return nodeIconFragment(iconElement);
+ } else {
+ return checkNodeIconFragment(iconColor, iconName, iconClassName);
+ }
+ }, [data, isEmoji, name, showNode]);
+
+ const nodeIconFragment = (icon) => {
+ return {icon} ;
+ };
+
+ const checkNodeIconFragment = (iconColor, iconName, iconClassName) => {
+ return (
+
+ );
+ };
+
+ const isDark = useDarkStore((state) => state.dark);
+ const renderIconStatus = (
+ buildStatus: BuildStatus | undefined,
+ validationStatus: validationStatusType | null,
+ ) => {
+ const conditionSuccess = validationStatus && validationStatus.valid;
+ const conditionInactive =
+ validationStatus &&
+ !validationStatus.valid &&
+ buildStatus === BuildStatus.INACTIVE;
+ const conditionError =
+ buildStatus === BuildStatus.ERROR ||
+ (validationStatus && !validationStatus.valid);
+
+ if (buildStatus === BuildStatus.BUILDING) {
+ return ;
+ } else {
+ return (
+ <>
+
+ {conditionSuccess ? (
+
+ ) : conditionInactive ? (
+
+ ) : conditionError ? (
+
+ ) : (
+ <>>
+ )}
+ >
+ );
+ }
+ };
+ const getSpecificClassFromBuildStatus = (
+ buildStatus: BuildStatus | undefined,
+ validationStatus: validationStatusType | null,
+ ) => {
+ let isInvalid = validationStatus && !validationStatus.valid;
+
+ if (buildStatus === BuildStatus.INACTIVE) {
+ // INACTIVE should have its own class
+ return "inactive-status";
+ }
+ 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";
+ } else {
+ return "";
+ }
+ };
+
+ const getNodeBorderClassName = (
+ selected: boolean,
+ showNode: boolean,
+ buildStatus: BuildStatus | undefined,
+ validationStatus: validationStatusType | null,
+ ) => {
+ const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
+ buildStatus,
+ validationStatus,
+ );
+
+ const baseBorderClass = getBaseBorderClass(selected);
+ const nodeSizeClass = getNodeSizeClass(showNode);
+ const names = classNames(
+ baseBorderClass,
+ nodeSizeClass,
+ "generic-node-div group/node",
+ specificClassFromBuildStatus,
+ );
+ return names;
+ };
+
+ const getBaseBorderClass = (selected) => {
+ let className = selected ? "border border-ring" : "border";
+ let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
+ return data.node?.frozen ? frozenClass : className;
+ };
+
+ const getNodeSizeClass = (showNode) =>
+ showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
+
+ const memoizedNodeToolbarComponent = useMemo(() => {
+ return (
+
+ {
+ takeSnapshot();
+ deleteNode(id);
+ }}
+ setShowNode={(show) => {
+ setNode(data.id, (old) => ({
+ ...old,
+ data: { ...old.data, showNode: show },
+ }));
+ }}
+ setShowState={setShowNode}
+ numberOfHandles={handles}
+ showNode={showNode}
+ openAdvancedModal={false}
+ onCloseAdvancedModal={() => {}}
+ updateNodeCode={updateNodeCode}
+ isOutdated={isOutdated}
+ selected={selected}
+ />
+
+ );
+ }, [
+ data,
+ deleteNode,
+ takeSnapshot,
+ setNode,
+ setShowNode,
+ handles,
+ showNode,
+ updateNodeCode,
+ isOutdated,
+ selected,
+ ]);
+ return (
+ <>
+ {memoizedNodeToolbarComponent}
+
+ {data.node?.beta && showNode && (
+
+ )}
+
+
+
+ {iconNodeRender()}
+ {showNode && (
+
+ {nameEditable && inputName ? (
+
+ {
+ setInputName(false);
+ if (nodeName.trim() !== "") {
+ setNodeName(nodeName);
+ setNode(data.id, (old) => ({
+ ...old,
+ data: {
+ ...old.data,
+ node: {
+ ...old.data.node,
+ display_name: nodeName,
+ },
+ },
+ }));
+ } else {
+ setNodeName(data.node!.display_name);
+ }
+ }}
+ value={nodeName}
+ onChange={setNodeName}
+ password={false}
+ blurOnEnter={true}
+ id={`input-title-${data.node?.display_name}`}
+ />
+
+ ) : (
+
+
+ {
+ if (nameEditable) {
+ setInputName(true);
+ }
+ takeSnapshot();
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ data-testid={"title-" + data.node?.display_name}
+ className="generic-node-tooltip-div cursor-text text-primary"
+ >
+ {data.node?.display_name}
+
+
+
+ )}
+
+ )}
+
+
+ {!showNode && (
+ <>
+ {Object.keys(data.node!.template)
+ .filter((templateField) => templateField.charAt(0) !== "_")
+ .map(
+ (templateField: string, idx) =>
+ data.node!.template[templateField].show &&
+ !data.node!.template[templateField].advanced && (
+
0
+ ? nodeColors[
+ data.node?.template[templateField]
+ .input_types![
+ data.node?.template[templateField]
+ .input_types!.length - 1
+ ]
+ ] ??
+ nodeColors[
+ types[
+ data.node?.template[templateField]
+ .input_types![
+ data.node?.template[templateField]
+ .input_types!.length - 1
+ ]
+ ]
+ ]
+ : nodeColors[
+ data.node?.template[templateField].type!
+ ] ??
+ nodeColors[
+ types[
+ data.node?.template[templateField].type!
+ ]
+ ] ??
+ nodeColors.unknown
+ }
+ title={getFieldTitle(
+ data.node?.template!,
+ templateField,
+ )}
+ info={data.node?.template[templateField].info}
+ name={templateField}
+ tooltipTitle={
+ data.node?.template[
+ templateField
+ ].input_types?.join("\n") ??
+ data.node?.template[templateField].type
+ }
+ required={
+ data.node!.template[templateField].required
+ }
+ id={{
+ inputTypes:
+ data.node!.template[templateField].input_types,
+ type: data.node!.template[templateField].type,
+ id: data.id,
+ fieldName: templateField,
+ }}
+ left={true}
+ type={data.node?.template[templateField].type}
+ optionalHandle={
+ data.node?.template[templateField].input_types
+ }
+ proxy={data.node?.template[templateField].proxy}
+ showNode={showNode}
+ />
+ ),
+ )}
+ {/* 0
+ ? data.node.output_types.join("|")
+ : data.type
+ }
+ tooltipTitle={data.node?.base_classes.join("\n")}
+ id={{
+ baseClasses: data.node!.base_classes,
+ id: data.id,
+ dataType: data.type,
+ idx: 0,
+ }}
+ type={data.node?.base_classes.join("|")}
+ left={false}
+ showNode={showNode}
+ /> */}
+ >
+ )}
+
+ {showNode && (
+
{STATUS_BUILDING}
+ ) : !validationStatus ? (
+ {STATUS_BUILD}
+ ) : (
+
+
+ {lastRunTime && (
+
+
{RUN_TIMESTAMP_PREFIX}
+
+ {lastRunTime}
+
+
+ )}
+
+
+
Duration:
+
+ {validationStatus?.data.duration}
+
+
+
+
+ Output
+
+
+ {validationString.split("\n").map((line, index) => (
+
+ {line}
+
+ ))}
+
+
+ )
+ }
+ side="bottom"
+ >
+ {
+ if (buildStatus === BuildStatus.BUILDING || isBuilding)
+ return;
+ setValidationStatus(null);
+ buildFlow({ stopNodeId: data.id });
+ }}
+ variant="secondary"
+ className={"group h-9 px-1.5"}
+ >
+
+
+ {renderIconStatus(buildStatus, validationStatus)}
+
+
+
+
+ )}
+
+
+
+ {showNode && (
+
+
+ {showNode && nameEditable && inputDescription ? (
+
+ <>
+ {Object.keys(data.node!.template)
+ .filter((templateField) => templateField.charAt(0) !== "_")
+ .sort((a, b) => sortFields(a, b, data.node?.field_order ?? []))
+ .map((templateField: string, idx) => (
+
+ {data.node!.template[templateField].show &&
+ !data.node!.template[templateField].advanced ? (
+
0
+ ? nodeColors[
+ data.node?.template[templateField].input_types![
+ data.node?.template[templateField]
+ .input_types!.length - 1
+ ]
+ ] ??
+ nodeColors[
+ types[
+ data.node?.template[templateField]
+ .input_types![
+ data.node?.template[templateField]
+ .input_types!.length - 1
+ ]
+ ]
+ ]
+ : nodeColors[
+ data.node?.template[templateField].type!
+ ] ??
+ nodeColors[
+ types[data.node?.template[templateField].type!]
+ ] ??
+ nodeColors.unknown
+ }
+ title={getFieldTitle(
+ data.node?.template!,
+ templateField,
+ )}
+ info={data.node?.template[templateField].info}
+ name={templateField}
+ tooltipTitle={
+ data.node?.template[templateField].input_types?.join(
+ "\n",
+ ) ?? data.node?.template[templateField].type
+ }
+ required={data.node!.template[templateField].required}
+ id={{
+ inputTypes:
+ data.node!.template[templateField].input_types,
+ type: data.node!.template[templateField].type,
+ id: data.id,
+ fieldName: templateField,
+ }}
+ left={true}
+ type={data.node?.template[templateField].type}
+ optionalHandle={
+ data.node?.template[templateField].input_types
+ }
+ proxy={data.node?.template[templateField].proxy}
+ showNode={showNode}
+ />
+ ) : (
+ <>>
+ )}
+
+ ))}
+
+ {" "}
+
+ {data.node!.outputs &&
+ data.node!.outputs.length > 0 &&
+ data.node!.outputs.map((output, idx) => (
+
+ ))}
+ >
+
+ )}
+
+ >
+ );
+}
diff --git a/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx b/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx
new file mode 100644
index 000000000..34d41313b
--- /dev/null
+++ b/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx
@@ -0,0 +1,52 @@
+import { cloneDeep } from "lodash";
+import { useEffect } from "react";
+import useAlertStore from "../../stores/alertStore";
+import { ResponseErrorDetailAPI } from "../../types/api";
+
+const useFetchDataOnMount = (
+ data,
+ name,
+ handleUpdateValues,
+ setNode,
+ setIsLoading
+) => {
+ const setErrorData = useAlertStore((state) => state.setErrorData);
+
+ useEffect(() => {
+ async function fetchData() {
+ if (
+ (data.node?.template[name]?.real_time_refresh ||
+ data.node?.template[name]?.refresh_button) &&
+ // options can be undefined but not an empty array
+ (data.node?.template[name]?.options?.length ?? 0) === 0
+ ) {
+ setIsLoading(true);
+ try {
+ let newTemplate = await handleUpdateValues(name, data);
+
+ if (newTemplate) {
+ setNode(data.id, (oldNode) => {
+ let newNode = cloneDeep(oldNode);
+ newNode.data = {
+ ...newNode.data,
+ };
+ newNode.data.node.template = newTemplate;
+ return newNode;
+ });
+ }
+ } catch (error) {
+ let responseError = error as ResponseErrorDetailAPI;
+
+ setErrorData({
+ title: "Error while updating the Component",
+ list: [responseError?.response?.data?.detail ?? "Unknown error"],
+ });
+ }
+ setIsLoading(false);
+ }
+ }
+ fetchData();
+ }, []); // Empty dependency array ensures that this effect runs only once, on mount
+};
+
+export default useFetchDataOnMount;
diff --git a/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx b/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx
new file mode 100644
index 000000000..1a394cad3
--- /dev/null
+++ b/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx
@@ -0,0 +1,73 @@
+import { cloneDeep } from "lodash";
+import useAlertStore from "../../stores/alertStore";
+import { ResponseErrorTypeAPI } from "../../types/api";
+
+const useHandleOnNewValue = (
+ data,
+ name,
+ takeSnapshot,
+ handleUpdateValues,
+ debouncedHandleUpdateValues,
+ setNode,
+ setIsLoading
+) => {
+ const setErrorData = useAlertStore((state) => state.setErrorData);
+
+ const handleOnNewValue = async (newValue, skipSnapshot = false) => {
+ const nodeTemplate = data.node!.template[name];
+ const currentValue = nodeTemplate.value;
+
+ if (currentValue !== newValue && !skipSnapshot) {
+ takeSnapshot();
+ }
+
+ const shouldUpdate =
+ data.node?.template[name].real_time_refresh &&
+ !data.node?.template[name].refresh_button &&
+ currentValue !== newValue;
+
+ const typeToDebounce = nodeTemplate.type;
+
+ nodeTemplate.value = newValue;
+
+ let newTemplate;
+ if (shouldUpdate) {
+ setIsLoading(true);
+ try {
+ if (["int"].includes(typeToDebounce)) {
+ newTemplate = await handleUpdateValues(name, data);
+ } else {
+ newTemplate = await debouncedHandleUpdateValues(name, data);
+ }
+ } catch (error) {
+ let responseError = error as ResponseErrorTypeAPI;
+ setErrorData({
+ title: "Error while updating the Component",
+ list: [
+ responseError?.response?.data?.detail.error ?? "Unknown error",
+ ],
+ });
+ }
+ setIsLoading(false);
+ }
+
+ setNode(data.id, (oldNode) => {
+ const newNode = cloneDeep(oldNode);
+ newNode.data = {
+ ...newNode.data,
+ };
+
+ if (data.node?.template[name].real_time_refresh && newTemplate) {
+ newNode.data.node.template = newTemplate;
+ } else {
+ newNode.data.node.template[name].value = newValue;
+ }
+
+ return newNode;
+ });
+ };
+
+ return { handleOnNewValue };
+};
+
+export default useHandleOnNewValue;
diff --git a/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx b/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx
new file mode 100644
index 000000000..c2afa1ab1
--- /dev/null
+++ b/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx
@@ -0,0 +1,37 @@
+import { cloneDeep } from "lodash";
+
+const useHandleNodeClass = (
+ data,
+ name,
+ takeSnapshot,
+ setNode,
+ updateNodeInternals
+) => {
+ const handleNodeClass = (newNodeClass, code) => {
+ if (!data.node) return;
+ if (data.node!.template[name].value !== code) {
+ takeSnapshot();
+ }
+
+ setNode(data.id, (oldNode) => {
+ let newNode = cloneDeep(oldNode);
+
+ newNode.data = {
+ ...newNode.data,
+ node: newNodeClass,
+ description: newNodeClass.description ?? data.node!.description,
+ display_name: newNodeClass.display_name ?? data.node!.display_name,
+ };
+
+ newNode.data.node.template[name].value = code;
+
+ return newNode;
+ });
+
+ updateNodeInternals(data.id);
+ };
+
+ return { handleNodeClass };
+};
+
+export default useHandleNodeClass;
diff --git a/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx b/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx
new file mode 100644
index 000000000..14e983ca1
--- /dev/null
+++ b/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx
@@ -0,0 +1,38 @@
+import { cloneDeep } from "lodash";
+import useAlertStore from "../../stores/alertStore";
+import { ResponseErrorDetailAPI } from "../../types/api";
+import { handleUpdateValues } from "../../utils/parameterUtils";
+
+const useHandleRefreshButtonPress = (setIsLoading, setNode) => {
+ const setErrorData = useAlertStore((state) => state.setErrorData);
+
+ const handleRefreshButtonPress = async (name, data) => {
+ setIsLoading(true);
+ try {
+ let newTemplate = await handleUpdateValues(name, data);
+
+ if (newTemplate) {
+ setNode(data.id, (oldNode) => {
+ let newNode = cloneDeep(oldNode);
+ newNode.data = {
+ ...newNode.data,
+ };
+ newNode.data.node.template = newTemplate;
+ return newNode;
+ });
+ }
+ } catch (error) {
+ let responseError = error as ResponseErrorDetailAPI;
+
+ setErrorData({
+ title: "Error while updating the Component",
+ list: [responseError?.response?.data?.detail ?? "Unknown error"],
+ });
+ }
+ setIsLoading(false);
+ };
+
+ return { handleRefreshButtonPress };
+};
+
+export default useHandleRefreshButtonPress;
diff --git a/src/frontend/src/CustomNodes/utils/get-field-title.tsx b/src/frontend/src/CustomNodes/utils/get-field-title.tsx
new file mode 100644
index 000000000..a00829a90
--- /dev/null
+++ b/src/frontend/src/CustomNodes/utils/get-field-title.tsx
@@ -0,0 +1,10 @@
+import { APITemplateType } from "../../types/api";
+
+export default function getFieldTitle(
+ template: APITemplateType,
+ templateField: string
+): string {
+ return template[templateField].display_name
+ ? template[templateField].display_name!
+ : template[templateField].name ?? templateField;
+}
diff --git a/src/frontend/src/CustomNodes/utils/sort-fields.tsx b/src/frontend/src/CustomNodes/utils/sort-fields.tsx
new file mode 100644
index 000000000..b432d57ed
--- /dev/null
+++ b/src/frontend/src/CustomNodes/utils/sort-fields.tsx
@@ -0,0 +1,40 @@
+import { priorityFields } from "../../constants/constants";
+
+export default function sortFields(a, b, fieldOrder) {
+ // Early return for empty fields
+ if (!a && !b) return 0;
+ if (!a) return 1;
+ if (!b) return -1;
+
+ // Normalize the case to ensure case-insensitive comparison
+ const normalizedFieldA = a.toLowerCase();
+ const normalizedFieldB = b.toLowerCase();
+
+ const aIsPriority = priorityFields.has(normalizedFieldA);
+ const bIsPriority = priorityFields.has(normalizedFieldB);
+
+ // Sort by priority
+ if (aIsPriority && !bIsPriority) return -1;
+ if (!aIsPriority && bIsPriority) return 1;
+
+ // Check if either field is in the fieldOrder array
+ const indexOfA = fieldOrder.indexOf(normalizedFieldA);
+ const indexOfB = fieldOrder.indexOf(normalizedFieldB);
+
+ // If both fields are in fieldOrder, sort by their order in the array
+ if (indexOfA !== -1 && indexOfB !== -1) {
+ return indexOfA - indexOfB;
+ }
+
+ // If only one of the fields is in fieldOrder, that field comes first
+ if (indexOfA !== -1) {
+ return -1;
+ }
+ if (indexOfB !== -1) {
+ return 1;
+ }
+
+ // Default case for fields not in priorityFields and not found in fieldOrder
+ // You might want to sort them alphabetically or in another specific manner
+ return a.localeCompare(b);
+}