From 36f806897c90fc413a48a637f0fd1f46f04c9fcd Mon Sep 17 00:00:00 2001 From: luccast <2213102+luccast@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:49:44 -0500 Subject: [PATCH] Refactor ActionGraph component to include ReactFlowProvider and enhance structure - Introduced ActionGraphInner as a separate component to utilize ReactFlow hooks. - Added ChaserCrab overlay to display additional node information. - Wrapped ActionGraph in ReactFlowProvider for improved context management. - Updated layout and styling for better visual consistency. --- src/components/monitor/ActionGraph.tsx | 21 +++- src/components/monitor/ChaserCrab.tsx | 153 +++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/components/monitor/ChaserCrab.tsx diff --git a/src/components/monitor/ActionGraph.tsx b/src/components/monitor/ActionGraph.tsx index 67a7e65..091fcaa 100644 --- a/src/components/monitor/ActionGraph.tsx +++ b/src/components/monitor/ActionGraph.tsx @@ -10,11 +10,13 @@ import { type Edge, type NodeTypes, MarkerType, + ReactFlowProvider, } from '@xyflow/react' import '@xyflow/react/dist/style.css' import { SessionNode } from './SessionNode' import { ActionNode } from './ActionNode' import { CrabNode } from './CrabNode' +import { ChaserCrab } from './ChaserCrab' import { layoutGraph } from '~/lib/graph-layout' import type { MonitorSession, MonitorAction } from '~/integrations/clawdbot' @@ -34,7 +36,8 @@ const nodeTypes: NodeTypes = { crab: CrabNode as any, } -export function ActionGraph({ +// Inner component that uses ReactFlow hooks +function ActionGraphInner({ sessions, actions, selectedSession, @@ -178,6 +181,9 @@ export function ActionGraph({ const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges) + // Get node IDs for the chaser crab + const nodeIds = useMemo(() => nodes.map((n) => n.id), [nodes]) + // Update nodes when layout changes useEffect(() => { setNodes(layoutedNodes) @@ -195,7 +201,7 @@ export function ActionGraph({ ) return ( -
+
+ {/* Chaser crab overlay */} +
) } + +// Wrapper that provides ReactFlowProvider +export function ActionGraph(props: ActionGraphProps) { + return ( + + + + ) +} diff --git a/src/components/monitor/ChaserCrab.tsx b/src/components/monitor/ChaserCrab.tsx new file mode 100644 index 0000000..bab51f2 --- /dev/null +++ b/src/components/monitor/ChaserCrab.tsx @@ -0,0 +1,153 @@ +import { useState, useEffect, useRef } from 'react' +import { useReactFlow } from '@xyflow/react' +import { CrabIdleAnimation, CrabJumpAnimation, CrabAttackAnimation } from '~/components/ani' + +type CrabState = 'idle' | 'running' | 'attacking' + +interface Position { + x: number + y: number +} + +interface ChaserCrabProps { + nodeIds: string[] +} + +const CRAB_SIZE = 20 +const CRAB_SPEED = 3 // pixels per frame +const ATTACK_DISTANCE = 30 // how close before attacking +const ATTACK_CHANCE = 0.4 // 40% chance to attack when close + +export function ChaserCrab({ nodeIds }: ChaserCrabProps) { + const { getNodes, getViewport } = useReactFlow() + + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [target, setTarget] = useState(null) + const [crabState, setCrabState] = useState('idle') + const [facingLeft, setFacingLeft] = useState(false) + + const prevNodeIdsRef = useRef>(new Set()) + const animationFrameRef = useRef(undefined) + const attackTimeoutRef = useRef(undefined) + + // Detect new nodes and set them as targets + useEffect(() => { + const currentIds = new Set(nodeIds) + const prevIds = prevNodeIdsRef.current + + // Find newly added nodes (excluding the central crab node) + for (const id of currentIds) { + if (!prevIds.has(id) && !id.includes('crab-origin')) { + // New node found! Get its position + const nodes = getNodes() + const newNode = nodes.find((n) => n.id === id) + if (newNode) { + const nodeCenter = { + x: newNode.position.x + 100, // Approximate center + y: newNode.position.y + 40, + } + setTarget(nodeCenter) + setCrabState('running') + + // Clear any existing attack timeout + if (attackTimeoutRef.current) { + clearTimeout(attackTimeoutRef.current) + } + } + break // Only chase one new node at a time + } + } + + prevNodeIdsRef.current = currentIds + }, [nodeIds, getNodes]) + + // Animation loop for movement + useEffect(() => { + if (!target || crabState === 'attacking') return + + const animate = () => { + setPosition((current) => { + const dx = target.x - current.x + const dy = target.y - current.y + const distance = Math.sqrt(dx * dx + dy * dy) + + // Check if we're close enough + if (distance < ATTACK_DISTANCE) { + // Randomly decide to attack or go idle + if (Math.random() < ATTACK_CHANCE) { + setCrabState('attacking') + // Return to idle after attack animation (4 frames at 10fps = 400ms) + attackTimeoutRef.current = setTimeout(() => { + setCrabState('idle') + setTarget(null) + }, 400) + } else { + setCrabState('idle') + setTarget(null) + } + return current + } + + // Move towards target + const vx = (dx / distance) * CRAB_SPEED + const vy = (dy / distance) * CRAB_SPEED + + // Update facing direction + if (Math.abs(vx) > 0.1) { + setFacingLeft(vx < 0) + } + + return { + x: current.x + vx, + y: current.y + vy, + } + }) + + animationFrameRef.current = requestAnimationFrame(animate) + } + + animationFrameRef.current = requestAnimationFrame(animate) + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + }, [target, crabState]) + + // Cleanup on unmount + useEffect(() => { + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + if (attackTimeoutRef.current) { + clearTimeout(attackTimeoutRef.current) + } + } + }, []) + + // Get viewport for proper positioning + const viewport = getViewport() + + // Transform crab position to screen coordinates + const screenX = position.x * viewport.zoom + viewport.x + const screenY = position.y * viewport.zoom + viewport.y + + return ( +
+ {crabState === 'idle' && } + {crabState === 'running' && } + {crabState === 'attacking' && } +
+ ) +}