From b6f21c210ee804782eba2e7c30c2ccdcbd95bffb Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Fri, 13 Feb 2026 05:07:46 +0000 Subject: Add unfold merged trace: convert to sequential node chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unfold takes a merged trace's messages, extracts the node order, and creates real edges chaining those nodes sequentially (A→B→C→D→E). The merged trace is deleted and replaced by a regular pass-through trace. - Add unfoldMergedTrace() to flowStore (creates edges, rewires downstream) - Add Unfold button (Layers icon) to Sidebar merged traces UI - Fix isMerged edge detection to use explicit flag instead of ID prefix - Fix LLMNode useUpdateNodeInternals deps for dynamic handle updates Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/nodes/LLMNode.tsx | 77 +++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 24 deletions(-) (limited to 'frontend/src/components/nodes') diff --git a/frontend/src/components/nodes/LLMNode.tsx b/frontend/src/components/nodes/LLMNode.tsx index 8cbf0e9..7542860 100644 --- a/frontend/src/components/nodes/LLMNode.tsx +++ b/frontend/src/components/nodes/LLMNode.tsx @@ -10,10 +10,10 @@ const LLMNode = ({ id, data, selected }: NodeProps) => { const updateNodeInternals = useUpdateNodeInternals(); const edges = useEdges(); - // Force update handles when traces change + // Force update handles when traces or edges change useEffect(() => { updateNodeInternals(id); - }, [id, data.outgoingTraces, data.mergedTraces, data.inputs, updateNodeInternals]); + }, [id, data.traces, data.outgoingTraces, data.mergedTraces, data.forkedTraces, data.inputs, edges.length, updateNodeInternals]); // Determine how many input handles to show // We want to ensure there is always at least one empty handle at the bottom @@ -53,6 +53,35 @@ const LLMNode = ({ id, data, selected }: NodeProps) => { const isDisabled = data.disabled; const isDark = theme === 'dark'; + // Calculate handle counts to determine minimum node height + // Each handle slot is 16px (h-3=12px + my-0.5*2=4px) + const HANDLE_SLOT = 16; + const NODE_PADDING = 16; // py-2 top + bottom + + // Left side: regular inputs + prepend handles + const prependCount = (data.outgoingTraces || []).filter(trace => { + const isSelfTrace = trace.id === `trace-${id}`; + const isForkTrace = trace.id.startsWith('fork-') && trace.sourceNodeId === id; + if (!isSelfTrace && !isForkTrace) return false; + return edges.some(e => e.source === id && e.sourceHandle === `trace-${trace.id}`); + }).length; + const leftHandleCount = inputsToShow + prependCount; + + // Right side: continue + outgoing + merged + new branch + const continueCount = (data.traces || []).filter((trace: any) => { + return !edges.some(e => e.source === id && e.sourceHandle === `trace-${trace.id}`); + }).length; + const outgoingCount = (data.outgoingTraces || []).filter(trace => { + const isLocallyMerged = data.mergedTraces?.some(m => m.id === trace.id); + if (isLocallyMerged) return false; + return edges.some(e => e.source === id && e.sourceHandle === `trace-${trace.id}`); + }).length; + const mergedCount = (data.mergedTraces || []).length; + const rightHandleCount = continueCount + outgoingCount + mergedCount + 1; // +1 for "new branch" + + const maxHandles = Math.max(leftHandleCount, rightHandleCount); + const minHandleHeight = maxHandles * HANDLE_SLOT + NODE_PADDING; + // Truncate preview content const previewContent = data.response ? data.response.slice(0, 200) + (data.response.length > 200 ? '...' : '') @@ -61,13 +90,13 @@ const LLMNode = ({ id, data, selected }: NodeProps) => { : null; return ( -
) => { ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200' }`} - style={{ pointerEvents: isDisabled ? 'none' : 'auto' }} + style={{ pointerEvents: isDisabled ? 'none' : 'auto', minHeight: minHandleHeight }} onMouseEnter={() => setShowPreview(true)} onMouseLeave={() => setShowPreview(false)} > @@ -145,16 +174,16 @@ const LLMNode = ({ id, data, selected }: NodeProps) => { const gradientStops = mergedColors.map((color, idx) => `${color} ${(idx / mergedColors.length) * 100}%, ${color} ${((idx + 1) / mergedColors.length) * 100}%` ).join(', '); - handleBackground = `linear-gradient(45deg, ${gradientStops})`; + handleBackground = `conic-gradient(from 0deg, ${gradientStops})`; } return ( -
+
) => { ); return ( -
+
) => { const gradientStops = colors.map((color: string, idx: number) => `${color} ${(idx / colors.length) * 100}%, ${color} ${((idx + 1) / colors.length) * 100}%` ).join(', '); - backgroundStyle = `linear-gradient(45deg, ${gradientStops})`; + backgroundStyle = `conic-gradient(from 0deg, ${gradientStops})`; } return ( -
+
) => { const gradientStops = colors.map((color, idx) => `${color} ${(idx / colors.length) * 100}%, ${color} ${((idx + 1) / colors.length) * 100}%` ).join(', '); - backgroundStyle = `linear-gradient(45deg, ${gradientStops})`; + backgroundStyle = `conic-gradient(from 0deg, ${gradientStops})`; } return ( -
+
) => { const gradientStops = colors.map((color, idx) => `${color} ${(idx / colors.length) * 100}%, ${color} ${((idx + 1) / colors.length) * 100}%` ).join(', '); - const stripeGradient = `linear-gradient(45deg, ${gradientStops})`; + const stripeGradient = `conic-gradient(from 0deg, ${gradientStops})`; return ( -
+
) => { })} {/* 3. New Branch Generator Handle (Always visible) */} -
+
-- cgit v1.2.3