import { BaseEdge, type EdgeProps, useInternalNode } from "@xyflow/react" import { memo, useEffect, useRef, useState } from "react" import { duration } from "@/lib/motion" import { cn } from "@/lib/utils" import { useLatestTs } from "./liveStore" export type BrainEdgeData = { /** Every message this connection carries, qualified. */ messages: string[] /** Circle radii of the two neurons: their centres, and where the line stops. */ sourceRadius: number targetRadius: number [key: string]: unknown } /** * Radius of the dot that marks where the connection meets a neuron. * * A flow node's port handle is 12px across on a node some four times that; a * neuron is 32px at its smallest, so the same proportion lands at 8px — small * enough that several connections into one small neuron stay separate dots. */ const DOT = 4 /** * A connection between neurons, lit by whatever passes along it. * * `LiveEdge` without the value chip or the producer check: a neuron can stand * for several nodes, so "did *this* node publish it" has no answer here, and a * circle the size of a coin has no room for a payload anyway. * * It runs centre to centre, so the line trims itself back to the two rims and * then further, to clear the dot that sits on each of them: a flow node's port, * filled where the value leaves and hollow where it arrives. The dots belong to * the edge rather than to the node because a neuron meets its connections all * round its rim, not at two fixed points — and being part of the edge is also * what makes them dim along with it. */ function BrainEdgeComponent({ id, source, target, data }: EdgeProps) { const { messages, sourceRadius, targetRadius } = (data ?? { messages: [], sourceRadius: 0, targetRadius: 0, }) as BrainEdgeData const sourceNode = useInternalNode(source) const targetNode = useInternalNode(target) const ts = useLatestTs(messages) const [pulsing, setPulsing] = useState(false) // Seeded with whatever the store already holds rather than with 0: the store // outlives this component, so a value that arrived before it mounted is // history and must not be replayed as a pulse. const lastTs = useRef(ts) // The two centres, taken from the layout rather than from the `sourceX`/ // `targetX` React Flow hands an edge. Those come off the handles, which it // measures out of the DOM exactly once per node — and here that once falls // inside the graph's entrance scale, so every one of them is stored a few // percent short of the centre and never measured again. A node's position // plus its radius is the same point, exactly, with nothing measured at all. const sourceX = (sourceNode?.internals.positionAbsolute.x ?? 0) + sourceRadius const sourceY = (sourceNode?.internals.positionAbsolute.y ?? 0) + sourceRadius const targetX = (targetNode?.internals.positionAbsolute.x ?? 0) + targetRadius const targetY = (targetNode?.internals.positionAbsolute.y ?? 0) + targetRadius const length = Math.hypot(targetX - sourceX, targetY - sourceY) || 1 const unitX = (targetX - sourceX) / length const unitY = (targetY - sourceY) / length // Neither trim reaches past the midpoint, so two neurons closer together than // their radii still get a line pointing the right way rather than a backwards // one. The force layout keeps them apart; this only stops it looking broken. const from = Math.min(sourceRadius, length / 2) const to = Math.min(targetRadius, length - from) const startX = sourceX + unitX * from const startY = sourceY + unitY * from const endX = targetX - unitX * to const endY = targetY - unitY * to // The line stops at each dot rather than running under it, so a filled dot is // one flat tone instead of a disc with a darker line showing through half of // it. Never longer than the gap, or a short connection would draw backwards. const trim = Math.min(DOT, (length - from - to) / 2) const path = `M${startX + unitX * trim},${startY + unitY * trim}L${endX - unitX * trim},${endY - unitY * trim}` useEffect(() => { if (!ts || ts === lastTs.current) return lastTs.current = ts setPulsing(true) const timer = setTimeout(() => setPulsing(false), duration.pulse * 1000) return () => clearTimeout(timer) }, [ts]) return ( // The tone lives on the group so the line and both dots share one falloff. ) } export const BrainEdge = memo(BrainEdgeComponent)