From 8bac1094c9136d870933f9ca849ce7ea97e019cd Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:39:31 -0300 Subject: [PATCH] fix: make not filled required fields be validated before sending to the backend (#8711) * Added validation for nodes before building, make validation message appear as toast instead of build error * Make subgraph validation not occur in flowStore * Added function to search for connected nodes down or upstream * Added validation before flow starts of the connected flows * Catch error when sending message and restore chat value * Made enter not behave as enter on chat input text area * made sendMessage be async and throw errors * Added build status error to the nodes that didn't make it past validation. * Fixed flow not running when not every edge is showed --------- Co-authored-by: Mike Fortman --- .../components/build-status-display.tsx | 6 ++ src/frontend/src/constants/constants.ts | 2 + .../chatView/chatInput/chat-input.tsx | 20 +++-- .../components/text-area-wrapper.tsx | 1 + .../chatView/components/chat-view.tsx | 4 +- .../src/modals/IOModal/playground-modal.tsx | 1 + src/frontend/src/stores/flowStore.ts | 73 ++++++++++--------- src/frontend/src/types/components/index.ts | 4 +- src/frontend/src/utils/buildUtils.ts | 1 - src/frontend/src/utils/reactflowUtils.ts | 41 +++++++++++ 10 files changed, 109 insertions(+), 44 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/components/build-status-display.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/components/build-status-display.tsx index e56b6d79a..78cd03d7e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/components/build-status-display.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/components/build-status-display.tsx @@ -3,6 +3,7 @@ import { STATUS_BUILD, STATUS_BUILDING, STATUS_INACTIVE, + STATUS_MISSING_FIELDS_ERROR, } from "@/constants/constants"; import { BuildStatus } from "@/constants/enums"; @@ -58,6 +59,11 @@ const BuildStatusDisplay = ({ return {STATUS_INACTIVE}; } + if (buildStatus === BuildStatus.ERROR && !validationStatus) { + // If the build status is error and there is no validation status, it means that it failed before building, so show the Missing Required Fields error message + return {STATUS_MISSING_FIELDS_ERROR}; + } + if (!validationStatus) { return {STATUS_BUILD}; } diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 1b908f88e..c8ecac2d9 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -735,6 +735,8 @@ export const INSERT_API_KEY = "Insert your Langflow API key."; export const INVALID_API_KEY = "Your API key is not valid. "; export const CREATE_API_KEY = `Don't have an API key? Sign up at`; export const STATUS_BUILD = "Build to validate status."; +export const STATUS_MISSING_FIELDS_ERROR = + "Please fill all the required fields."; export const STATUS_INACTIVE = "Execution blocked"; export const STATUS_BUILDING = "Building..."; export const SAVED_HOVER = "Last saved: "; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx index e2796cc93..435f231b1 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx @@ -162,11 +162,21 @@ export default function ChatInput({ }; }, [handleFileChange, currentFlowId, isBuilding]); - const send = () => { - sendMessage({ - repeat: 1, - files: files.map((file) => file.path ?? "").filter((file) => file !== ""), - }); + const setChatValueStore = useUtilityStore((state) => state.setChatValueStore); + + const send = async () => { + const storedChatValue = chatValue; + try { + await sendMessage({ + repeat: 1, + files: files + .map((file) => file.path ?? "") + .filter((file) => file !== ""), + }); + } catch (error) { + setChatValueStore(storedChatValue); + } + setFiles([]); }; diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/text-area-wrapper.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/text-area-wrapper.tsx index 9060aa0f4..4d9132c5c 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/text-area-wrapper.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/text-area-wrapper.tsx @@ -46,6 +46,7 @@ const TextAreaWrapper = ({ data-testid="input-chat-playground" onKeyDown={(event) => { if (checkSendingOk(event)) { + event.preventDefault(); send(); } }} diff --git a/src/frontend/src/modals/IOModal/components/chatView/components/chat-view.tsx b/src/frontend/src/modals/IOModal/components/chatView/components/chat-view.tsx index 236e14dd0..a014608a8 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/components/chat-view.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/components/chat-view.tsx @@ -294,8 +294,8 @@ export default function ChatView({ { - sendMessage({ repeat, files }); + sendMessage={async ({ repeat, files }) => { + await sendMessage({ repeat, files }); track("Playground Message Sent"); }} inputRef={ref} diff --git a/src/frontend/src/modals/IOModal/playground-modal.tsx b/src/frontend/src/modals/IOModal/playground-modal.tsx index 6d2b941f5..728b7bc38 100644 --- a/src/frontend/src/modals/IOModal/playground-modal.tsx +++ b/src/frontend/src/modals/IOModal/playground-modal.tsx @@ -227,6 +227,7 @@ export default function IOModal({ eventDelivery: eventDeliveryConfig, }).catch((err) => { console.error(err); + throw err; }); } }, diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 839e39bb9..2b4588db4 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -39,6 +39,7 @@ import { checkChatInput, cleanEdges, detectBrokenEdgesEdges, + getConnectedSubgraph, getHandleId, getNodeId, scapeJSONParse, @@ -658,52 +659,56 @@ const useFlowStore = create((set, get) => ({ get().setIsBuilding(true); set({ flowBuildStatus: {} }); const currentFlow = useFlowsManagerStore.getState().currentFlow; - const setSuccessData = useAlertStore.getState().setSuccessData; const setErrorData = useAlertStore.getState().setErrorData; const edges = get().edges; - let error = false; let errors: string[] = []; - for (const edge of edges) { - const errorsEdge = validateEdge(edge, get().nodes, edges); + + // Only validate upstream nodes/edges if startNodeId is provided + let nodesToValidate = get().nodes; + let edgesToValidate = edges; + if (startNodeId) { + const downstream = getConnectedSubgraph( + startNodeId, + get().nodes, + edges, + "downstream", + ); + nodesToValidate = downstream.nodes; + edgesToValidate = downstream.edges; + } else if (stopNodeId) { + const upstream = getConnectedSubgraph( + stopNodeId, + get().nodes, + edges, + "upstream", + ); + nodesToValidate = upstream.nodes; + edgesToValidate = upstream.edges; + } + + for (const edge of edgesToValidate) { + const errorsEdge = validateEdge(edge, nodesToValidate, edgesToValidate); if (errorsEdge.length > 0) { - error = true; errors.push(errorsEdge.join("\n")); - useAlertStore.getState().addNotificationToHistory({ - title: MISSED_ERROR_ALERT, - type: "error", - list: errorsEdge, - }); } } - if (error) { + const errorsObjs = validateNodes(nodesToValidate, edges); + + errors = errors.concat(errorsObjs.map((obj) => obj.errors).flat()); + if (errors.length > 0) { + setErrorData({ + title: MISSED_ERROR_ALERT, + list: errors, + }); + const ids = errorsObjs.map((obj) => obj.id).flat(); + get().updateBuildStatus(ids, BuildStatus.ERROR); // Set only the build status as error without adding info to the flow pool + get().setIsBuilding(false); - get().setBuildInfo({ error: errors, success: false }); throw new Error("Invalid components"); } - function validateSubgraph(nodes: string[]) { - const errorsObjs = validateNodes( - get().nodes.filter((node) => nodes.includes(node.id)), - get().edges, - ); - - const errors = errorsObjs.map((obj) => obj.errors).flat(); - if (errors.length > 0) { - get().setBuildInfo({ error: errors, success: false }); - useAlertStore.getState().addNotificationToHistory({ - title: MISSED_ERROR_ALERT, - type: "error", - list: errors, - }); - get().setIsBuilding(false); - const ids = errorsObjs.map((obj) => obj.id).flat(); - - get().updateBuildStatus(ids, BuildStatus.ERROR); - throw new Error("Invalid components"); - } - // get().updateEdgesRunningByNodes(nodes, true); - } + function validateSubgraph() {} function handleBuildUpdate( vertexBuildData: VertexBuildTypeAPI, status: BuildStatus, diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 87716e3d6..8c3860ada 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -551,7 +551,7 @@ export type ChatInputType = { }: { repeat: number; files?: string[]; - }) => void; + }) => Promise; playgroundPage: boolean; }; @@ -840,7 +840,7 @@ export type chatViewProps = { }: { repeat: number; files?: string[]; - }) => void; + }) => Promise; visibleSession?: string; focusChat?: string; closeChat?: () => void; diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 51304b1bd..2eb1a1880 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -1,6 +1,5 @@ import { MISSED_ERROR_ALERT } from "@/constants/alerts_constants"; import { - BASE_URL_API, BUILD_POLLING_INTERVAL, POLLING_MESSAGES, } from "@/constants/constants"; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 218902df9..0c0a2cddb 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -2094,3 +2094,44 @@ export function buildPositionDictionary(nodes: AllNodeType[]) { export function hasStreaming(nodes: AllNodeType[]) { return nodes.some((node) => node.data.node?.template?.stream?.value); } + +// Utility to get all connected nodes and edges from a given nodeId, in a given direction +export function getConnectedSubgraph( + nodeId: string, + nodes: AllNodeType[], + edges: EdgeType[], + direction: "upstream" | "downstream", +): { nodes: AllNodeType[]; edges: EdgeType[] } { + const visited = new Set(); + const resultNodes: AllNodeType[] = []; + const resultEdges: EdgeType[] = []; + + function dfs(currentId: string) { + if (visited.has(currentId)) return; + visited.add(currentId); + const node = nodes.find((n) => n.id === currentId); + if (node) { + resultNodes.push(node); + if (direction === "upstream") { + // Find all incoming edges + const incomingEdges = edges.filter((e) => e.target === currentId); + for (const edge of incomingEdges) { + resultEdges.push(edge); + dfs(edge.source); + } + } else { + // downstream: Find all outgoing edges + const outgoingEdges = edges.filter((e) => e.source === currentId); + for (const edge of outgoingEdges) { + resultEdges.push(edge); + dfs(edge.target); + } + } + } + } + dfs(nodeId); + return { + nodes: resultNodes, + edges: resultEdges, + }; +}