From f0172a22dfe93a744b16938cb0f8ab679241393a Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Wed, 5 Nov 2025 16:37:48 -0500 Subject: [PATCH] various Validation Block buffs (#3919) --- skyvern-frontend/src/components/NoticeMe.tsx | 92 +++++++++++++++++++ .../editor/nodes/components/MicroDropdown.tsx | 2 +- .../editor/nodes/components/NodeHeader.tsx | 35 +++---- .../panels/WorkflowNodeLibraryPanel.tsx | 4 +- 4 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 skyvern-frontend/src/components/NoticeMe.tsx diff --git a/skyvern-frontend/src/components/NoticeMe.tsx b/skyvern-frontend/src/components/NoticeMe.tsx new file mode 100644 index 00000000..710413dc --- /dev/null +++ b/skyvern-frontend/src/components/NoticeMe.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef, useState } from "react"; + +interface Props { + trigger: "render" | "viewport"; +} + +function NoticeMe({ children, trigger }: React.PropsWithChildren) { + const [shouldAnimate, setShouldAnimate] = useState(trigger === "render"); + const [shouldHide, setShouldHide] = useState(false); + const elementRef = useRef(null); + const hasExitedRef = useRef(false); + + useEffect(() => { + if (trigger !== "viewport") return; + + const element = elementRef.current; + if (!element) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + // Element is visible in viewport + if (hasExitedRef.current) { + // Force animation restart by removing then re-adding class + setShouldHide(false); + setShouldAnimate(false); + // Use setTimeout to ensure the class is removed before re-adding + setTimeout(() => { + setShouldAnimate(true); + }, 10); + hasExitedRef.current = false; + } + } else { + // Element is NOT visible in viewport (completely outside) + setShouldAnimate(false); + setShouldHide(true); + hasExitedRef.current = true; + } + }); + }, + { + threshold: 0, + rootMargin: "0px", + }, + ); + + observer.observe(element); + + return () => observer.disconnect(); + }, [trigger]); + + const getAnimationClass = () => { + if (shouldHide) return "notice-me-hidden"; + if (shouldAnimate) return "notice-me-animate"; + return ""; + }; + + return ( + <> +
+ {children} +
+ + + ); +} + +export { NoticeMe }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/MicroDropdown.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/MicroDropdown.tsx index 99799fa3..4da607cb 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/MicroDropdown.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/MicroDropdown.tsx @@ -61,7 +61,7 @@ function MicroDropdown({ selections, selected, onChange }: Props) {
{ if (!isOpen && dropdownRef.current) { const rect = dropdownRef.current.getBoundingClientRect(); diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx index da0d2706..f0f00256 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx @@ -6,6 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { getClient } from "@/api/AxiosClient"; import { ProxyLocation, Status } from "@/api/types"; +import { NoticeMe } from "@/components/NoticeMe"; import { StatusBadge } from "@/components/StatusBadge"; import { toast } from "@/components/ui/use-toast"; import { useLogging } from "@/hooks/useLogging"; @@ -531,24 +532,26 @@ function NodeHeader({ {transmutations.blockTitle} - t.label), - ]} - selected={transmutations.self} - onChange={(label) => { - const transmutation = transmutations.others.find( - (t) => t.label === label, - ); + + t.label), + ]} + selected={transmutations.self} + onChange={(label) => { + const transmutation = transmutations.others.find( + (t) => t.label === label, + ); - if (!transmutation) { - return; - } + if (!transmutation) { + return; + } - transmuteNodeCallback(nodeId, transmutation.nodeName); - }} - /> + transmuteNodeCallback(nodeId, transmutation.nodeName); + }} + /> +
) : ( {blockTitle} diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index ded8ab0a..f0fdaaa7 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -84,8 +84,8 @@ const nodeLibraryItems: Array<{ className="size-6" /> ), - title: "Validation Block", - description: "Validate completion criteria", + title: "AI or Human Validation", + description: "Have an AI or Human validate the state of the screen", }, /** * The Human Interaction block can be had via a transmutation of the