mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-20 12:29:14 +08:00
feat(visualization): add workflow run animation logic
This commit is contained in:
parent
2a02f01580
commit
eabf1f2080
@ -9,20 +9,202 @@ import {
|
|||||||
useEdgesState,
|
useEdgesState,
|
||||||
Handle,
|
Handle,
|
||||||
Position,
|
Position,
|
||||||
|
type Node,
|
||||||
|
type Edge,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
|
||||||
import {
|
import {
|
||||||
Brain,
|
Brain,
|
||||||
FilePen,
|
FilePen,
|
||||||
MessageSquareQuote,
|
MessageSquareQuote,
|
||||||
Microscope,
|
Microscope,
|
||||||
|
RotateCcw,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
Users,
|
Users,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
} from "react";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
import { ShineBorder } from "~/components/magicui/shine-border";
|
import { ShineBorder } from "~/components/magicui/shine-border";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { useIntersectionObserver } from "~/hooks/use-intersection-observer";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 75;
|
||||||
|
const ROW_1 = 0;
|
||||||
|
const ROW_2 = ROW_HEIGHT;
|
||||||
|
const ROW_3 = ROW_HEIGHT * 2;
|
||||||
|
const ROW_4 = ROW_HEIGHT * 2;
|
||||||
|
const ROW_5 = ROW_HEIGHT * 3;
|
||||||
|
const ROW_6 = ROW_HEIGHT * 4;
|
||||||
|
|
||||||
|
export type WorkflowNode = Node<{
|
||||||
|
label: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
active?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const initialNodes: WorkflowNode[] = [
|
||||||
|
{
|
||||||
|
id: "Start",
|
||||||
|
type: "circle",
|
||||||
|
data: { label: "Start" },
|
||||||
|
position: { x: 25, y: ROW_1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coordinator",
|
||||||
|
data: { icon: MessageSquareQuote, label: "Coordinator" },
|
||||||
|
position: { x: 150, y: ROW_1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Planner",
|
||||||
|
data: { icon: Brain, label: "Planner" },
|
||||||
|
position: { x: 150, y: ROW_2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Reporter",
|
||||||
|
data: { icon: FilePen, label: "Reporter" },
|
||||||
|
position: { x: 275, y: ROW_3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "HumanFeedback",
|
||||||
|
data: { icon: UserCheck, label: "Human Feedback" },
|
||||||
|
position: { x: 25, y: ROW_4 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam",
|
||||||
|
data: { icon: Users, label: "Research Team" },
|
||||||
|
position: { x: 25, y: ROW_5 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Researcher",
|
||||||
|
data: { icon: Microscope, label: "Researcher" },
|
||||||
|
position: { x: -75, y: ROW_6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coder",
|
||||||
|
data: { icon: SquareTerminal, label: "Coder" },
|
||||||
|
position: { x: 125, y: ROW_6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "End",
|
||||||
|
type: "circle",
|
||||||
|
data: { label: "End" },
|
||||||
|
position: { x: 330, y: ROW_6 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: "Start->Coordinator",
|
||||||
|
source: "Start",
|
||||||
|
target: "Coordinator",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "left",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coordinator->Planner",
|
||||||
|
source: "Coordinator",
|
||||||
|
target: "Planner",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Planner->Reporter",
|
||||||
|
source: "Planner",
|
||||||
|
target: "Reporter",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Planner->HumanFeedback",
|
||||||
|
source: "Planner",
|
||||||
|
target: "HumanFeedback",
|
||||||
|
sourceHandle: "left",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "HumanFeedback->Planner",
|
||||||
|
source: "HumanFeedback",
|
||||||
|
target: "Planner",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "bottom",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "HumanFeedback->ResearchTeam",
|
||||||
|
source: "HumanFeedback",
|
||||||
|
target: "ResearchTeam",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Reporter->End",
|
||||||
|
source: "Reporter",
|
||||||
|
target: "End",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam->Researcher",
|
||||||
|
source: "ResearchTeam",
|
||||||
|
target: "Researcher",
|
||||||
|
sourceHandle: "left",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam->Coder",
|
||||||
|
source: "ResearchTeam",
|
||||||
|
target: "Coder",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "left",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam->Planner",
|
||||||
|
source: "ResearchTeam",
|
||||||
|
target: "Planner",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "bottom",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Researcher->ResearchTeam",
|
||||||
|
source: "Researcher",
|
||||||
|
target: "ResearchTeam",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "bottom",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coder->ResearchTeam",
|
||||||
|
source: "Coder",
|
||||||
|
target: "ResearchTeam",
|
||||||
|
sourceHandle: "top",
|
||||||
|
targetHandle: "right",
|
||||||
|
animated: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
circle: CircleNode,
|
||||||
|
agent: AgentNode,
|
||||||
|
default: AgentNode,
|
||||||
|
};
|
||||||
|
|
||||||
const WORKFLOW_STEPS = [
|
const WORKFLOW_STEPS = [
|
||||||
{
|
{
|
||||||
@ -84,172 +266,80 @@ const WORKFLOW_STEPS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROW_HEIGHT = 75;
|
function useWorkflowRun(
|
||||||
const ROW_1 = 0;
|
setNodes: Dispatch<SetStateAction<WorkflowNode[]>>,
|
||||||
const ROW_2 = ROW_HEIGHT;
|
setEdges: Dispatch<SetStateAction<Edge[]>>,
|
||||||
const ROW_3 = ROW_HEIGHT * 2;
|
) {
|
||||||
const ROW_4 = ROW_HEIGHT * 2;
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const ROW_5 = ROW_HEIGHT * 3;
|
|
||||||
const ROW_6 = ROW_HEIGHT * 4;
|
|
||||||
const initialNodes = [
|
|
||||||
{
|
|
||||||
id: "Start",
|
|
||||||
type: "circle",
|
|
||||||
data: { label: "Start" },
|
|
||||||
position: { x: 25, y: ROW_1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coordinator",
|
|
||||||
data: { icon: MessageSquareQuote, label: "Coordinator" },
|
|
||||||
position: { x: 150, y: ROW_1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Planner",
|
|
||||||
data: { icon: Brain, label: "Planner" },
|
|
||||||
position: { x: 150, y: ROW_2 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Reporter",
|
|
||||||
data: { icon: FilePen, label: "Reporter" },
|
|
||||||
position: { x: 275, y: ROW_3 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "HumanFeedback",
|
|
||||||
data: { icon: UserCheck, label: "Human Feedback" },
|
|
||||||
position: { x: 25, y: ROW_4 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam",
|
|
||||||
data: { icon: Users, label: "Research Team" },
|
|
||||||
position: { x: 25, y: ROW_5 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Researcher",
|
|
||||||
data: { icon: Microscope, label: "Researcher" },
|
|
||||||
position: { x: -75, y: ROW_6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coder",
|
|
||||||
data: { icon: SquareTerminal, label: "Coder" },
|
|
||||||
position: { x: 125, y: ROW_6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "End",
|
|
||||||
type: "circle",
|
|
||||||
data: { label: "End" },
|
|
||||||
position: { x: 330, y: ROW_6 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialEdges = [
|
const clearAnimation = useCallback(() => {
|
||||||
{
|
setEdges((edges) => {
|
||||||
id: "Start->Coordinator",
|
return edges.map((edge) => ({
|
||||||
source: "Start",
|
...edge,
|
||||||
target: "Coordinator",
|
animated: false,
|
||||||
sourceHandle: "right",
|
}));
|
||||||
targetHandle: "left",
|
});
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coordinator->Planner",
|
|
||||||
source: "Coordinator",
|
|
||||||
target: "Planner",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Planner->Reporter",
|
|
||||||
source: "Planner",
|
|
||||||
target: "Reporter",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Planner->HumanFeedback",
|
|
||||||
source: "Planner",
|
|
||||||
target: "HumanFeedback",
|
|
||||||
sourceHandle: "left",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "HumanFeedback->Planner",
|
|
||||||
source: "HumanFeedback",
|
|
||||||
target: "Planner",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "bottom",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "HumanFeedback->ResearchTeam",
|
|
||||||
source: "HumanFeedback",
|
|
||||||
target: "ResearchTeam",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Reporter->End",
|
|
||||||
source: "Reporter",
|
|
||||||
target: "End",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam->Researcher",
|
|
||||||
source: "ResearchTeam",
|
|
||||||
target: "Researcher",
|
|
||||||
sourceHandle: "left",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam->Coder",
|
|
||||||
source: "ResearchTeam",
|
|
||||||
target: "Coder",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "left",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam->Planner",
|
|
||||||
source: "ResearchTeam",
|
|
||||||
target: "Planner",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "bottom",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Researcher->ResearchTeam",
|
|
||||||
source: "Researcher",
|
|
||||||
target: "ResearchTeam",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "bottom",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coder->ResearchTeam",
|
|
||||||
source: "Coder",
|
|
||||||
target: "ResearchTeam",
|
|
||||||
sourceHandle: "top",
|
|
||||||
targetHandle: "right",
|
|
||||||
animated: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const nodeTypes = {
|
setNodes((nodes) => {
|
||||||
circle: CircleNode,
|
return nodes.map((node) => ({
|
||||||
agent: AgentNode,
|
...node,
|
||||||
default: AgentNode,
|
data: { ...node.data, active: false },
|
||||||
};
|
}));
|
||||||
|
});
|
||||||
|
}, [setEdges, setNodes]);
|
||||||
|
|
||||||
|
const run = useCallback(async () => {
|
||||||
|
setIsRunning(true);
|
||||||
|
clearAnimation();
|
||||||
|
for (const step of WORKFLOW_STEPS) {
|
||||||
|
setNodes((nodes) => {
|
||||||
|
return nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
active: step.activeNodes.includes(node.id),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
setEdges((edges) => {
|
||||||
|
return edges.map((edge) => ({
|
||||||
|
...edge,
|
||||||
|
animated: step.activeEdges.includes(edge.id),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
clearAnimation();
|
||||||
|
setIsRunning(false);
|
||||||
|
}, [setNodes, setEdges, clearAnimation]);
|
||||||
|
|
||||||
|
return { run, isRunning };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
export function MultiAgentVisualization() {
|
export function MultiAgentVisualization() {
|
||||||
const [nodes, setNodes] = useNodesState(initialNodes);
|
const [nodes, setNodes] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges] = useEdgesState(initialEdges);
|
const [edges, setEdges] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
const { run, isRunning } = useWorkflowRun(setNodes, setEdges);
|
||||||
|
const [hasAutoRunned, setHasAutoRunned] = useState(false);
|
||||||
|
|
||||||
|
const { ref } = useIntersectionObserver({
|
||||||
|
rootMargin: "0%",
|
||||||
|
threshold: 0,
|
||||||
|
onChange: (isIntersecting) => {
|
||||||
|
if (isIntersecting && !hasAutoRunned) {
|
||||||
|
setHasAutoRunned(true);
|
||||||
|
void run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
style={{
|
style={{
|
||||||
@ -259,7 +349,7 @@ export function MultiAgentVisualization() {
|
|||||||
edges={edges}
|
edges={edges}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
attributionPosition="top-right"
|
proOptions={{ hideAttribution: true }}
|
||||||
colorMode="dark"
|
colorMode="dark"
|
||||||
panOnScroll={false}
|
panOnScroll={false}
|
||||||
zoomOnScroll={false}
|
zoomOnScroll={false}
|
||||||
@ -270,6 +360,26 @@ export function MultiAgentVisualization() {
|
|||||||
className="[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]"
|
className="[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]"
|
||||||
bgColor="var(--background)"
|
bgColor="var(--background)"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-0 left-0 z-10"
|
||||||
|
disabled={isRunning}
|
||||||
|
onClick={() => {
|
||||||
|
void run();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCcw
|
||||||
|
className={cn({
|
||||||
|
"animate-spin": isRunning,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
id="auto-run-animation-trigger"
|
||||||
|
ref={ref}
|
||||||
|
className="absolute bottom-0 left-[50%] h-px w-px"
|
||||||
|
/>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
137
web/src/hooks/use-intersection-observer.ts
Normal file
137
web/src/hooks/use-intersection-observer.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
isIntersecting: boolean;
|
||||||
|
entry?: IntersectionObserverEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseIntersectionObserverOptions = {
|
||||||
|
root?: Element | Document | null;
|
||||||
|
rootMargin?: string;
|
||||||
|
threshold?: number | number[];
|
||||||
|
freezeOnceVisible?: boolean;
|
||||||
|
onChange?: (
|
||||||
|
isIntersecting: boolean,
|
||||||
|
entry: IntersectionObserverEntry,
|
||||||
|
) => void;
|
||||||
|
initialIsIntersecting?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IntersectionReturn = [
|
||||||
|
(node?: Element | null) => void,
|
||||||
|
boolean,
|
||||||
|
IntersectionObserverEntry | undefined,
|
||||||
|
] & {
|
||||||
|
ref: (node?: Element | null) => void;
|
||||||
|
isIntersecting: boolean;
|
||||||
|
entry?: IntersectionObserverEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useIntersectionObserver({
|
||||||
|
threshold = 0,
|
||||||
|
root = null,
|
||||||
|
rootMargin = "0%",
|
||||||
|
freezeOnceVisible = false,
|
||||||
|
initialIsIntersecting = false,
|
||||||
|
onChange,
|
||||||
|
}: UseIntersectionObserverOptions = {}): IntersectionReturn {
|
||||||
|
const [ref, setRef] = useState<Element | null>(null);
|
||||||
|
|
||||||
|
const [state, setState] = useState<State>(() => ({
|
||||||
|
isIntersecting: initialIsIntersecting,
|
||||||
|
entry: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const callbackRef =
|
||||||
|
useRef<UseIntersectionObserverOptions["onChange"]>(undefined);
|
||||||
|
|
||||||
|
callbackRef.current = onChange;
|
||||||
|
|
||||||
|
const frozen = state.entry?.isIntersecting && freezeOnceVisible;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Ensure we have a ref to observe
|
||||||
|
if (!ref) return;
|
||||||
|
|
||||||
|
// Ensure the browser supports the Intersection Observer API
|
||||||
|
if (!("IntersectionObserver" in window)) return;
|
||||||
|
|
||||||
|
// Skip if frozen
|
||||||
|
if (frozen) return;
|
||||||
|
|
||||||
|
let unobserve: (() => void) | undefined;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries: IntersectionObserverEntry[]): void => {
|
||||||
|
const thresholds = Array.isArray(observer.thresholds)
|
||||||
|
? observer.thresholds
|
||||||
|
: [observer.thresholds];
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const isIntersecting =
|
||||||
|
entry.isIntersecting &&
|
||||||
|
thresholds.some(
|
||||||
|
(threshold) => entry.intersectionRatio >= threshold,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState({ isIntersecting, entry });
|
||||||
|
|
||||||
|
if (callbackRef.current) {
|
||||||
|
callbackRef.current(isIntersecting, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIntersecting && freezeOnceVisible && unobserve) {
|
||||||
|
unobserve();
|
||||||
|
unobserve = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold, root, rootMargin },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(ref);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
ref,
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
JSON.stringify(threshold),
|
||||||
|
root,
|
||||||
|
rootMargin,
|
||||||
|
frozen,
|
||||||
|
freezeOnceVisible,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ensures that if the observed element changes, the intersection observer is reinitialized
|
||||||
|
const prevRef = useRef<Element | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!ref &&
|
||||||
|
state.entry?.target &&
|
||||||
|
!freezeOnceVisible &&
|
||||||
|
!frozen &&
|
||||||
|
prevRef.current !== state.entry.target
|
||||||
|
) {
|
||||||
|
prevRef.current = state.entry.target;
|
||||||
|
setState({ isIntersecting: initialIsIntersecting, entry: undefined });
|
||||||
|
}
|
||||||
|
}, [ref, state.entry, freezeOnceVisible, frozen, initialIsIntersecting]);
|
||||||
|
|
||||||
|
const result = [
|
||||||
|
setRef,
|
||||||
|
!!state.isIntersecting,
|
||||||
|
state.entry,
|
||||||
|
] as IntersectionReturn;
|
||||||
|
|
||||||
|
// Support object destructuring, by adding the specific values.
|
||||||
|
result.ref = result[0];
|
||||||
|
result.isIntersecting = result[1];
|
||||||
|
result.entry = result[2];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user