From 28b38b324b023d6a87bb0e4994a96008e11b1d7e Mon Sep 17 00:00:00 2001 From: ogabrielluiz Date: Thu, 6 Jun 2024 18:23:56 -0300 Subject: [PATCH] Add CustomNodes folder --- .../parameterComponent/constants.ts | 1 + .../components/parameterComponent/index.tsx | 584 ++++++++++++ .../tooltipRenderComponent/index.tsx | 91 ++ .../src/CustomNodes/GenericNode/index.tsx | 849 ++++++++++++++++++ .../hooks/use-fetch-data-on-mount.tsx | 54 ++ .../hooks/use-handle-new-value.tsx | 76 ++ .../hooks/use-handle-node-class.tsx | 40 + .../hooks/use-handle-refresh-buttons.tsx | 39 + .../src/CustomNodes/utils/get-field-title.tsx | 10 + .../src/CustomNodes/utils/sort-fields.tsx | 40 + 10 files changed, 1784 insertions(+) create mode 100644 src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/constants.ts create mode 100644 src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx create mode 100644 src/frontend/src/CustomNodes/GenericNode/components/tooltipRenderComponent/index.tsx create mode 100644 src/frontend/src/CustomNodes/GenericNode/index.tsx create mode 100644 src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx create mode 100644 src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx create mode 100644 src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx create mode 100644 src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx create mode 100644 src/frontend/src/CustomNodes/utils/get-field-title.tsx create mode 100644 src/frontend/src/CustomNodes/utils/sort-fields.tsx 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..dfa0a6812 --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -0,0 +1,584 @@ +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, + TOOLTIP_EMPTY, +} from "../../../../constants/constants"; +import { Case } from "../../../../shared/components/caseComponent"; +import useAlertStore from "../../../../stores/alertStore"; +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 TooltipRenderComponent from "../tooltipRenderComponent"; +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 = "", +}: ParameterComponentType): JSX.Element { + const ref = useRef(null); + const refHtml = useRef(null); + const infoHtml = useRef(null); + const currentFlow = useFlowsManagerStore((state) => state.currentFlow); + 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 flow = currentFlow?.data?.nodes ?? null; + const groupedEdge = useRef(null); + const setFilterEdge = useFlowStore((state) => state.setFilterEdge); + + const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue( + data, + name, + takeSnapshot, + handleUpdateValues, + debouncedHandleUpdateValues, + setNode, + renderTooltips, + setIsLoading + ); + + const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass( + data, + name, + takeSnapshot, + setNode, + updateNodeInternals, + renderTooltips + ); + + const { handleRefreshButtonPress: handleRefreshButtonPressHook } = + useHandleRefreshButtonPress(setIsLoading, setNode, renderTooltips); + + 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, + renderTooltips, + 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 renderTooltips() { + let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, flow!); + groupedEdge.current = groupedObj; + + if (groupedObj && groupedObj.length > 0) { + //@ts-ignore + refHtml.current = groupedObj.map((item, index) => { + return ; + }); + } else { + //@ts-ignore + refHtml.current = ( + {TOOLTIP_EMPTY} + ); + } + } + + // If optionalHandle is an empty list, then it is not an optional handle + if (optionalHandle && optionalHandle.length === 0) { + optionalHandle = null; + } + + useEffect(() => { + renderTooltips(); + }, [tooltipTitle, flow]); + + return !showNode ? ( + left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? ( + <> + ) : ( + + ) + ) : ( + + ); +} 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..ba0d1c9a6 --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -0,0 +1,849 @@ +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 + ) => { + if (buildStatus === BuildStatus.BUILDING) { + return ; + } else { + return ( + <> + + {validationStatus && validationStatus.valid ? ( + + ) : validationStatus && + !validationStatus.valid && + buildStatus === BuildStatus.INACTIVE ? ( + + ) : buildStatus === BuildStatus.ERROR || + (validationStatus && !validationStatus.valid) ? ( + + ) : ( + + )} + + ); + } + }; + 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", + 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 && ( +
+
BETA
+
+ )} +
+
+
+ {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, + }} + 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" + > + +
+ )} +
+
+ + {showNode && ( +
+
+ {showNode && nameEditable && inputDescription ? ( +