Add CrabNode to ActionGraph for enhanced session visualization

- Introduced CrabNode to represent a central point in the ActionGraph.
- Updated nodeTypes to include CrabNode and modified rawNodes to always include it.
- Established connections from CrabNode to visible session nodes for better clarity.
- Adjusted layout logic to ensure proper positioning of nodes, especially when only the CrabNode is present.
- Updated MiniMap to visually distinguish CrabNode with a unique color.
This commit is contained in:
luccast
2026-01-25 18:21:07 -05:00
parent 642d424da5
commit fe2120fd8d
4 changed files with 69 additions and 31 deletions
+39 -2
View File
@@ -14,6 +14,7 @@ import {
import '@xyflow/react/dist/style.css'
import { SessionNode } from './SessionNode'
import { ActionNode } from './ActionNode'
import { CrabNode } from './CrabNode'
import { layoutGraph } from '~/lib/graph-layout'
import type { MonitorSession, MonitorAction } from '~/integrations/clawdbot'
@@ -24,10 +25,13 @@ interface ActionGraphProps {
onSessionSelect: (key: string | null) => void
}
const CRAB_NODE_ID = 'crab-origin'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeTypes: NodeTypes = {
session: SessionNode as any,
action: ActionNode as any,
crab: CrabNode as any,
}
export function ActionGraph({
@@ -46,6 +50,15 @@ export function ActionGraph({
const rawNodes = useMemo(() => {
const nodes: Node[] = []
// Always add the central crab node
const hasActivity = sessions.length > 0 || visibleActions.length > 0
nodes.push({
id: CRAB_NODE_ID,
type: 'crab',
position: { x: 0, y: 0 },
data: { active: hasActivity },
})
// Add session nodes
const visibleSessions = selectedSession
? sessions.filter((s) => s.key === selectedSession)
@@ -77,6 +90,22 @@ export function ActionGraph({
const rawEdges = useMemo(() => {
const edges: Edge[] = []
// Get visible sessions for edge creation
const visibleSessions = selectedSession
? sessions.filter((s) => s.key === selectedSession)
: sessions
// Connect crab to each session
for (const session of visibleSessions) {
edges.push({
id: `e-crab-${session.key}`,
source: CRAB_NODE_ID,
target: `session-${session.key}`,
markerEnd: { type: MarkerType.ArrowClosed },
style: { stroke: '#06b6d4', strokeWidth: 2 },
})
}
// Group actions by runId
const runActions = new Map<string, MonitorAction[]>()
for (const action of visibleActions) {
@@ -117,11 +146,18 @@ export function ActionGraph({
}
return edges
}, [visibleActions])
}, [sessions, visibleActions, selectedSession])
// Apply layout
const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
if (rawNodes.length === 0) return { nodes: [], edges: [] }
// Always have at least the crab node
if (rawNodes.length === 1) {
// Just the crab node, center it
return {
nodes: [{ ...rawNodes[0]!, position: { x: 0, y: 0 } }],
edges: [],
}
}
return layoutGraph(rawNodes, rawEdges, {
direction: 'TB',
nodeWidth: 200,
@@ -170,6 +206,7 @@ export function ActionGraph({
<MiniMap
className="bg-gray-800! border-gray-700! rounded-lg!"
nodeColor={(node) => {
if (node.type === 'crab') return '#f97316' // orange for crab
if (node.type === 'session') return '#06b6d4'
return '#6b7280'
}}
+23
View File
@@ -0,0 +1,23 @@
import { Handle, Position } from '@xyflow/react'
import { motion } from 'framer-motion'
interface CrabNodeProps {
data: { active: boolean }
}
export function CrabNode({ data }: CrabNodeProps) {
return (
<motion.div
className="relative flex items-center justify-center"
animate={data.active ? { scale: [1, 1.05, 1] } : {}}
transition={{ repeat: Infinity, duration: 2 }}
>
<div className="text-6xl select-none">🦀</div>
<Handle
type="source"
position={Position.Bottom}
className="bg-cyan-500! w-3! h-3! border-2! border-gray-900!"
/>
</motion.div>
)
}
+1
View File
@@ -2,5 +2,6 @@ export { ActionGraph } from './ActionGraph'
export { SessionList } from './SessionList'
export { SessionNode } from './SessionNode'
export { ActionNode } from './ActionNode'
export { CrabNode } from './CrabNode'
export { StatusIndicator } from './StatusIndicator'
export { SettingsPanel } from './SettingsPanel'
+6 -29
View File
@@ -189,35 +189,12 @@ function MonitorPage() {
{/* Graph area */}
<div className="flex-1">
{sessions.length === 0 && !connecting ? (
<div className="h-full flex items-center justify-center text-gray-500">
<div className="text-center">
<Activity size={48} className="mx-auto mb-4 opacity-50" />
<p className="text-lg mb-2">No active sessions</p>
<p className="text-sm">
{connected
? 'Waiting for agent activity...'
: 'Connect to clawdbot gateway to start monitoring'}
</p>
{!connected && (
<button
onClick={handleConnect}
disabled={connecting}
className="mt-4 px-4 py-2 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-sm font-medium disabled:opacity-50"
>
Connect
</button>
)}
</div>
</div>
) : (
<ActionGraph
sessions={sessions}
actions={actions}
selectedSession={selectedSession}
onSessionSelect={setSelectedSession}
/>
)}
<ActionGraph
sessions={sessions}
actions={actions}
selectedSession={selectedSession}
onSessionSelect={setSelectedSession}
/>
</div>
</div>
</div>