import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath, useStore, } from "@xyflow/react" import { memo, useEffect, useRef, useState } from "react" import { duration } from "@/lib/motion" import { cn } from "@/lib/utils" import type { FlowEdgeData } from "./deriveEdges" import { useLiveValue } from "./liveStore" /** Below this zoom the value chips would be unreadable, so they step aside. */ const CHIP_MIN_ZOOM = 0.5 function formatValue(value: unknown): string { if (typeof value === "number") { return Number.isInteger(value) ? String(value) : value.toFixed(2) } if (typeof value === "string") return value return JSON.stringify(value) ?? "" } function LiveEdgeComponent({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, selected, }: EdgeProps) { const { message, producerId } = (data ?? {}) as FlowEdgeData const live = useLiveValue(message) const zoom = useStore((state) => state.transform[2]) const [pulsing, setPulsing] = useState(false) const lastTs = useRef(null) const [path, labelX, labelY] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, }) // Restart the stroke animation whenever a newer message lands — but only // for the producer that actually published it. A message can have several // producers, and can also be set from a dashboard or another flow, so // pulsing on the value alone claims things happened that did not. useEffect(() => { if (!live?.ts || live.ts === lastTs.current) return lastTs.current = live.ts const from = live.source // No source at all is an older engine; pulse rather than go silent. if (from && from.id !== producerId) return setPulsing(true) const timer = setTimeout(() => setPulsing(false), duration.pulse * 1000) return () => clearTimeout(timer) }, [live?.ts, live?.source, producerId]) return ( <> {live !== undefined && zoom >= CHIP_MIN_ZOOM ? (
{formatValue(live.value)}
) : null} ) } export const LiveEdge = memo(LiveEdgeComponent)