feat: render cross-reference edges as dashed purple arrows on canvas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-28 19:25:16 -05:00
parent ddf75df976
commit b0347bacd4

View File

@@ -29,6 +29,46 @@ function estimateNodeHeight(node: TreeStructure): number {
return height
}
/** Collect all node IDs in the tree. */
function collectAllIds(root: TreeStructure): Set<string> {
const ids = new Set<string>()
function walk(node: TreeStructure) {
ids.add(node.id)
node.children?.forEach(walk)
}
walk(root)
return ids
}
/** Find all cross-reference edges (next_node_id pointing outside children). */
function collectCrossRefEdges(root: TreeStructure): Array<{ source: string; target: string; label?: string }> {
const refs: Array<{ source: string; target: string; label?: string }> = []
const allIds = collectAllIds(root)
function walk(node: TreeStructure) {
const childIds = new Set(node.children?.map(c => c.id) ?? [])
// Decision options pointing outside children
if (node.type === 'decision' && node.options) {
for (const opt of node.options) {
if (opt.next_node_id && !childIds.has(opt.next_node_id) && allIds.has(opt.next_node_id)) {
refs.push({ source: node.id, target: opt.next_node_id, label: opt.label })
}
}
}
// Action next_node_id pointing to non-child (always a cross-ref since actions use next_node_id not children)
if (node.type === 'action' && node.next_node_id && allIds.has(node.next_node_id) && !childIds.has(node.next_node_id)) {
refs.push({ source: node.id, target: node.next_node_id, label: 'loops back' })
}
node.children?.forEach(walk)
}
walk(root)
return refs
}
interface UseTreeLayoutResult {
nodes: Node[]
edges: Edge[]
@@ -128,6 +168,40 @@ export function useTreeLayout(): UseTreeLayoutResult {
walk(treeStructure, null)
// Add cross-reference edges (dashed, purple)
if (treeStructure) {
const crossRefs = collectCrossRefEdges(treeStructure)
for (const ref of crossRefs) {
// Only add if both source and target nodes are visible (not collapsed away)
const sourceVisible = nodes.some(n => n.id === ref.source)
const targetVisible = nodes.some(n => n.id === ref.target)
if (sourceVisible && targetVisible) {
edges.push({
id: `xref-${ref.source}->${ref.target}`,
source: ref.source,
target: ref.target,
type: 'smoothstep',
animated: true,
label: ref.label ? truncateLabel(ref.label) : undefined,
labelStyle: { fill: 'hsl(var(--primary))', fontSize: 10, fontWeight: 500 },
labelBgStyle: { fill: 'hsl(var(--card))', fillOpacity: 0.95 },
labelBgPadding: [4, 2] as [number, number],
style: {
stroke: 'hsl(var(--primary))',
strokeWidth: 2,
strokeDasharray: '6 3',
},
markerEnd: {
type: 'arrowclosed' as const,
color: 'hsl(var(--primary))',
width: 16,
height: 16,
},
})
}
}
}
return { rawNodes: nodes, rawEdges: edges }
}, [treeStructure, collapsedNodeIds, validationErrors, measuredHeights])