summaryrefslogtreecommitdiff
path: root/frontend/src/components/nodes/LLMNode.tsx
diff options
context:
space:
mode:
authorYurenHao0426 <blackhao0426@gmail.com>2026-02-13 05:07:46 +0000
committerYurenHao0426 <blackhao0426@gmail.com>2026-02-13 05:07:46 +0000
commitb6f21c210ee804782eba2e7c30c2ccdcbd95bffb (patch)
tree4ff355d72a511063ba366c5052300cf1ca6f60a6 /frontend/src/components/nodes/LLMNode.tsx
parent7d897ad9bb5ee46839ec91992cbbf4593168f119 (diff)
Add unfold merged trace: convert to sequential node chain
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 <noreply@anthropic.com>
Diffstat (limited to 'frontend/src/components/nodes/LLMNode.tsx')
-rw-r--r--frontend/src/components/nodes/LLMNode.tsx77
1 files changed, 53 insertions, 24 deletions
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<NodeData>) => {
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<NodeData>) => {
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<NodeData>) => {
: null;
return (
- <div
+ <div
className={`px-4 py-2 shadow-md rounded-md border-2 min-w-[200px] transition-all relative ${
- isDisabled
- ? isDark
- ? 'bg-gray-800 border-gray-600 opacity-50 cursor-not-allowed'
+ isDisabled
+ ? isDark
+ ? 'bg-gray-800 border-gray-600 opacity-50 cursor-not-allowed'
: 'bg-gray-100 border-gray-300 opacity-50 cursor-not-allowed'
- : selected
+ : selected
? isDark
? 'bg-gray-800 border-blue-400'
: 'bg-white border-blue-500'
@@ -75,7 +104,7 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => {
? '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<NodeData>) => {
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 (
- <div key={i} className="relative h-4 w-4 my-1">
+ <div key={i} className="relative h-3 w-3 my-0.5">
<Handle
type="target"
position={Position.Left}
id={`input-${i}`}
- className="!w-3 !h-3 !left-[-6px] !border-0"
+ className="!w-2.5 !h-2.5 !left-[-6px] !border-0"
style={{
top: '50%',
transform: 'translateY(-50%)',
@@ -196,12 +225,12 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => {
);
return (
- <div key={`prepend-${trace.id}`} className="relative h-4 w-4 my-1" title={`Prepend context to: ${trace.id}`}>
+ <div key={`prepend-${trace.id}`} className="relative h-3 w-3 my-0.5" title={`Prepend context to: ${trace.id}`}>
<Handle
type="target"
position={Position.Left}
id={`prepend-${trace.id}`}
- className="!w-3 !h-3 !left-[-6px] !border-2"
+ className="!w-2.5 !h-2.5 !left-[-6px] !border-2"
style={{
top: '50%',
transform: 'translateY(-50%)',
@@ -244,16 +273,16 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => {
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 (
- <div key={`continue-${trace.id}`} className="relative h-4 w-4 my-1" title={`Continue trace: ${trace.id}`}>
+ <div key={`continue-${trace.id}`} className="relative h-3 w-3 my-0.5" title={`Continue trace: ${trace.id}`}>
<Handle
type="source"
position={Position.Right}
id={`trace-${trace.id}`}
- className="!w-3 !h-3 !right-[-6px]"
+ className="!w-2.5 !h-2.5 !right-[-6px]"
style={{
background: backgroundStyle,
top: '50%',
@@ -287,16 +316,16 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => {
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 (
- <div key={trace.id} className="relative h-4 w-4 my-1" title={`Trace: ${trace.id}`}>
+ <div key={trace.id} className="relative h-3 w-3 my-0.5" title={`Trace: ${trace.id}`}>
<Handle
type="source"
position={Position.Right}
id={`trace-${trace.id}`}
- className="!w-3 !h-3 !right-[-6px]"
+ className="!w-2.5 !h-2.5 !right-[-6px]"
style={{
background: backgroundStyle,
top: '50%',
@@ -319,15 +348,15 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => {
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 (
- <div key={merged.id} className="relative h-4 w-4 my-1" title={`Merged: ${merged.strategy} (${merged.sourceTraceIds.length} traces)`}>
+ <div key={merged.id} className="relative h-3 w-3 my-0.5" title={`Merged: ${merged.strategy} (${merged.sourceTraceIds.length} traces)`}>
<Handle
type="source"
position={Position.Right}
id={`trace-${merged.id}`}
- className="!w-3 !h-3 !right-[-6px]"
+ className="!w-2.5 !h-2.5 !right-[-6px]"
style={{
background: stripeGradient,
top: '50%',
@@ -340,12 +369,12 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => {
})}
{/* 3. New Branch Generator Handle (Always visible) */}
- <div className="relative h-4 w-4 my-1" title="Create New Branch">
+ <div className="relative h-3 w-3 my-0.5" title="Create New Branch">
<Handle
type="source"
position={Position.Right}
id="new-trace"
- className="!w-3 !h-3 !bg-gray-400 !right-[-6px]"
+ className="!w-2.5 !h-2.5 !bg-gray-400 !right-[-6px]"
style={{ top: '50%', transform: 'translateY(-50%)' }}
/>
<span className={`absolute right-4 top-[-2px] text-[9px] pointer-events-none w-max ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>