From 391ed6a00167bc3a1414490b891d5f2dda598e28 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Tue, 28 May 2024 12:15:11 -0300 Subject: [PATCH] feat: Add OutputModal component for displaying output visualization The new OutputModal component is added to the genericNode module. It provides a modal window for displaying the output visualization. The component includes a header, content, and footer sections. The content section includes the SwitchOutputView component, which renders different output views based on the type of results. The footer section includes an "Ok" button for closing the modal. --- .../components/switchOutputView/index.tsx | 38 ++ .../components/outputModal/index.tsx | 25 + .../src/customNodes/genericNode/index.tsx | 477 ++++++------------ 3 files changed, 226 insertions(+), 314 deletions(-) create mode 100644 src/frontend/src/customNodes/genericNode/components/outputModal/components/switchOutputView/index.tsx create mode 100644 src/frontend/src/customNodes/genericNode/components/outputModal/index.tsx diff --git a/src/frontend/src/customNodes/genericNode/components/outputModal/components/switchOutputView/index.tsx b/src/frontend/src/customNodes/genericNode/components/outputModal/components/switchOutputView/index.tsx new file mode 100644 index 000000000..ee09a9983 --- /dev/null +++ b/src/frontend/src/customNodes/genericNode/components/outputModal/components/switchOutputView/index.tsx @@ -0,0 +1,38 @@ +import RecordsOutputComponent from "../../../../../../components/recordsOutputComponent"; +import { Case } from "../../../../../../shared/components/caseComponent"; +import TextOutputView from "../../../../../../shared/components/textOutputView"; +import useFlowStore from "../../../../../../stores/flowStore"; + +export default function SwitchOutputView(nodeId): JSX.Element { + const nodes = useFlowStore((state) => state.nodes); + const setNode = useFlowStore((state) => state.setNode); + const flowPool = useFlowStore((state) => state.flowPool); + const node = nodes.find((node) => node?.id === nodeId?.nodeId); + + const flowPoolNode = (flowPool[node!.id] ?? [])[ + (flowPool[node!.id]?.length ?? 1) - 1 + ]; + + const results = flowPoolNode?.data?.logs[0] ?? ""; + + const checkType = () => { + const typeOutput = typeof results; + return typeOutput; + }; + + return ( + <> + +
NO OUTPUT
+
+ + + + + + + + + + ); +} diff --git a/src/frontend/src/customNodes/genericNode/components/outputModal/index.tsx b/src/frontend/src/customNodes/genericNode/components/outputModal/index.tsx new file mode 100644 index 000000000..fbddc0275 --- /dev/null +++ b/src/frontend/src/customNodes/genericNode/components/outputModal/index.tsx @@ -0,0 +1,25 @@ +import { Button } from "../../../../components/ui/button"; +import BaseModal from "../../../../modals/baseModal"; +import SwitchOutputView from "./components/switchOutputView"; + +export default function OutputModal({ open, setOpen, nodeId }): JSX.Element { + return ( + + +
+ Output View +
+
+ + + + +
+ +
+
+
+ ); +} diff --git a/src/frontend/src/customNodes/genericNode/index.tsx b/src/frontend/src/customNodes/genericNode/index.tsx index afaefb530..d17da3063 100644 --- a/src/frontend/src/customNodes/genericNode/index.tsx +++ b/src/frontend/src/customNodes/genericNode/index.tsx @@ -1,16 +1,11 @@ -import { cloneDeep } from "lodash"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { 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 { - NATIVE_CATEGORIES, RUN_TIMESTAMP_PREFIX, STATUS_BUILD, STATUS_BUILDING, @@ -22,26 +17,33 @@ import { useDarkStore } from "../../stores/darkStore"; import useFlowStore from "../../stores/flowStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { useTypesStore } from "../../stores/typesStore"; -import { APIClassType, VertexBuildTypeAPI } from "../../types/api"; -import { validationStatusType } from "../../types/components"; +import { VertexBuildTypeAPI } from "../../types/api"; import { NodeDataType } from "../../types/flow"; import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils"; import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils"; import { classNames, cn } from "../../utils/utils"; -import ParameterComponent from "./components/parameterComponent"; +import { countHandlesFn } from "../helpers/count-handles"; +import { getSpecificClassFromBuildStatus } from "../helpers/get-class-from-build-status"; +import useCheckCodeValidity from "../hooks/use-check-code-validity"; +import useIconNodeRender from "../hooks/use-icon-render"; +import useIconStatus from "../hooks/use-icons-status"; +import useUpdateNodeCode from "../hooks/use-update-node-code"; +import useUpdateValidationStatus from "../hooks/use-update-validation-status"; +import useValidationStatusString from "../hooks/use-validation-status-string"; import getFieldTitle from "../utils/get-field-title"; import sortFields from "../utils/sort-fields"; +import OutputModal from "./components/outputModal"; +import ParameterComponent from "./components/parameterComponent"; export default function GenericNode({ data, - xPos, - yPos, + selected, }: { data: NodeDataType; selected: boolean; - xPos: number; - yPos: number; + xPos?: number; + yPos?: number; }): JSX.Element { const types = useTypesStore((state) => state.types); const templates = useTypesStore((state) => state.templates); @@ -51,203 +53,42 @@ export default function GenericNode({ 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 isDark = useDarkStore((state) => state.dark); const buildStatus = useFlowStore( (state) => state.flowBuildStatus[data.id]?.status, ); const lastRunTime = useFlowStore( (state) => state.flowBuildStatus[data.id]?.timestamp, ); + const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); + + 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 [openOutputModal, setOpenOutputModal] = useState(false); + const [isOutdated, setIsOutdated] = useState(false); 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 ( - !NATIVE_CATEGORIES.includes(types[data.type]) || - !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", "Prompt"]; - 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]); - + const iconStatus = useIconStatus(buildStatus, validationStatus); + const [showNode, setShowNode] = useState(data.showNode ?? true); // 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): string => { - if (duration === undefined) { - return ""; - } else { - return `${duration}`; - } - }; - const durationString = getDurationString(validationStatus?.data.duration); + const updateNodeCode = useUpdateNodeCode( + data?.id, + data.node!, + setNode, + setIsOutdated, + updateNodeInternals, + ); - 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) { - console.log(relevantData) - // Extract validation information from relevantData and update the validationStatus state - setValidationStatus(relevantData); - } else { - setValidationStatus(null); - } - }, [flowPool[data.id], data.id]); - - useEffect(() => { - if (validationStatus?.data.logs) { - // if it is not a string turn it into a string - let newValidationString = ""; - if (Array.isArray(validationStatus.data.logs)) { - newValidationString = validationStatus.data.logs - .map((log) => log.message) - .join("\n"); - } - if (typeof newValidationString !== "string") { - newValidationString = JSON.stringify(validationStatus.data.logs); - } - - setValidationString(newValidationString); - } - }, [validationStatus, validationStatus?.data.logs]); - - const [showNode, setShowNode] = useState(data.showNode ?? true); - - useEffect(() => { - setShowNode(data.showNode ?? true); - }, [data.showNode]); - - const nameEditable = true; - - const emojiRegex = /\p{Emoji}/u; - 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 name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const nodeIconFragment = (icon) => { return {icon}; @@ -263,79 +104,24 @@ export default function GenericNode({ ); }; - const isDark = useDarkStore((state) => state.dark); - const renderIconStatus = ( - buildStatus: BuildStatus | undefined, - validationStatus: VertexBuildTypeAPI | 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: VertexBuildTypeAPI | 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 renderIconStatus = () => { + return ( +
+ {iconStatus} +
+ ); }; const getNodeBorderClassName = ( selected: boolean, showNode: boolean, buildStatus: BuildStatus | undefined, - validationStatus: VertexBuildTypeAPI | null + validationStatus: VertexBuildTypeAPI | null, ) => { const specificClassFromBuildStatus = getSpecificClassFromBuildStatus( buildStatus, validationStatus, + isDark, ); const baseBorderClass = getBaseBorderClass(selected); @@ -354,6 +140,65 @@ export default function GenericNode({ const getNodeSizeClass = (showNode) => showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full"; + const nameEditable = true; + const emojiRegex = /\p{Emoji}/u; + const isEmoji = emojiRegex.test(data?.node?.icon!); + + 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); + } + + useCheckCodeValidity(data, templates, setIsOutdated, types); + useValidationStatusString(validationStatus, setValidationString); + useUpdateValidationStatus(data?.id, flowPool, setValidationStatus); + + const iconNodeRender = useIconNodeRender( + data, + types, + nodeColors, + name, + showNode, + isEmoji, + nodeIconFragment, + checkNodeIconFragment, + ); + + function countHandles(): void { + const count = countHandlesFn(data); + setHandles(count); + } + + useEffect(() => { + countHandles(); + }, [data, data.node]); + + useEffect(() => { + if (!selected) { + setInputName(false); + setInputDescription(false); + } + }, [selected]); + + useEffect(() => { + setNodeDescription(data.node!.description); + }, [data.node!.description]); + + useEffect(() => { + setNodeName(data.node!.display_name); + }, [data.node!.display_name]); + + useEffect(() => { + setShowNode(data.showNode ?? true); + }, [data.showNode]); + const memoizedNodeToolbarComponent = useMemo(() => { return ( @@ -603,67 +448,63 @@ export default function GenericNode({ )} {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" - > + <> -
+ {STATUS_BUILDING} + ) : !validationStatus ? ( + {STATUS_BUILD} + ) : ( +
+
+ {lastRunTime && ( +
+
{RUN_TIMESTAMP_PREFIX}
+
+ {lastRunTime} +
+
+ )} +
+
+
Duration:
+
+ {validationStatus?.data.duration} +
+
+
+ ) + } + side="bottom" + > + +
+ )} @@ -865,6 +706,14 @@ export default function GenericNode({ )} + + {openOutputModal && ( + + )} ); }