various Validation Block buffs (#3919)
This commit is contained in:
92
skyvern-frontend/src/components/NoticeMe.tsx
Normal file
92
skyvern-frontend/src/components/NoticeMe.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
trigger: "render" | "viewport";
|
||||
}
|
||||
|
||||
function NoticeMe({ children, trigger }: React.PropsWithChildren<Props>) {
|
||||
const [shouldAnimate, setShouldAnimate] = useState(trigger === "render");
|
||||
const [shouldHide, setShouldHide] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(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 (
|
||||
<>
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={getAnimationClass()}
|
||||
style={{ display: "flex" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<style>{`
|
||||
.notice-me-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.notice-me-animate {
|
||||
animation: notice-fade-up 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes notice-fade-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { NoticeMe };
|
||||
@@ -61,7 +61,7 @@ function MicroDropdown({ selections, selected, onChange }: Props) {
|
||||
<div ref={dropdownRef} className="relative inline-block">
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className="relative inline-flex p-0 text-xs text-slate-400"
|
||||
className="relative inline-flex p-0 text-xs text-[#00d2ff]"
|
||||
onClick={() => {
|
||||
if (!isOpen && dropdownRef.current) {
|
||||
const rect = dropdownRef.current.getBoundingClientRect();
|
||||
|
||||
@@ -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({
|
||||
<span className="text-xs text-slate-400">
|
||||
{transmutations.blockTitle}
|
||||
</span>
|
||||
<MicroDropdown
|
||||
selections={[
|
||||
transmutations.self,
|
||||
...transmutations.others.map((t) => t.label),
|
||||
]}
|
||||
selected={transmutations.self}
|
||||
onChange={(label) => {
|
||||
const transmutation = transmutations.others.find(
|
||||
(t) => t.label === label,
|
||||
);
|
||||
<NoticeMe trigger="viewport">
|
||||
<MicroDropdown
|
||||
selections={[
|
||||
transmutations.self,
|
||||
...transmutations.others.map((t) => 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);
|
||||
}}
|
||||
/>
|
||||
</NoticeMe>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-slate-400">{blockTitle}</span>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user