From f97b7a1bfa220a0947f2cd63c23f4faa9fcd42e7 Mon Sep 17 00:00:00 2001 From: blackhao <13851610112@163.com> Date: Mon, 8 Dec 2025 15:07:12 -0600 Subject: merge logic --- frontend/src/components/nodes/LLMNode.tsx | 259 +++++++++++++++++++++++++++--- 1 file changed, 239 insertions(+), 20 deletions(-) (limited to 'frontend/src/components/nodes/LLMNode.tsx') diff --git a/frontend/src/components/nodes/LLMNode.tsx b/frontend/src/components/nodes/LLMNode.tsx index 592ab5b..dce1f2e 100644 --- a/frontend/src/components/nodes/LLMNode.tsx +++ b/frontend/src/components/nodes/LLMNode.tsx @@ -1,18 +1,19 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Handle, Position, type NodeProps, useUpdateNodeInternals, useEdges } from 'reactflow'; -import type { NodeData } from '../../store/flowStore'; +import type { NodeData, MergedTrace } from '../../store/flowStore'; import { Loader2, MessageSquare } from 'lucide-react'; import useFlowStore from '../../store/flowStore'; const LLMNode = ({ id, data, selected }: NodeProps) => { - const { updateNodeData } = useFlowStore(); + const { theme, nodes } = useFlowStore(); + const [showPreview, setShowPreview] = useState(false); const updateNodeInternals = useUpdateNodeInternals(); const edges = useEdges(); // Force update handles when traces change useEffect(() => { updateNodeInternals(id); - }, [id, data.outgoingTraces, data.inputs, updateNodeInternals]); + }, [id, data.outgoingTraces, data.mergedTraces, data.inputs, updateNodeInternals]); // Determine how many input handles to show // We want to ensure there is always at least one empty handle at the bottom @@ -50,68 +51,260 @@ const LLMNode = ({ id, data, selected }: NodeProps) => { const inputsToShow = Math.max(maxConnectedIndex + 2, 1); const isDisabled = data.disabled; + const isDark = theme === 'dark'; + + // Truncate preview content + const previewContent = data.response + ? data.response.slice(0, 200) + (data.response.length > 200 ? '...' : '') + : data.userPrompt + ? data.userPrompt.slice(0, 100) + (data.userPrompt.length > 100 ? '...' : '') + : null; return (
setShowPreview(true)} + onMouseLeave={() => setShowPreview(false)} > + {/* Content Preview Tooltip */} + {showPreview && previewContent && !isDisabled && ( +
+
+ {data.response ? 'Response Preview' : 'Prompt Preview'} +
+ {previewContent} + {/* Arrow */} +
+
+ )} +
-
+
{data.status === 'loading' ? ( ) : ( - + )}
-
+
{data.label} {isDisabled && (disabled)}
-
{data.model}
+
{data.model}
{/* Dynamic Inputs */}
+ {/* Regular input handles */} {Array.from({ length: inputsToShow }).map((_, i) => { // Find the connected edge to get color const connectedEdge = edges.find(e => e.target === id && e.targetHandle === `input-${i}`); const edgeColor = connectedEdge?.style?.stroke as string; + // Check if this is a merged trace connection + const edgeData = (connectedEdge as any)?.data; + const isMergedTrace = edgeData?.isMerged; + const mergedColors = edgeData?.colors as string[] | undefined; + + // Create gradient for merged traces + let handleBackground: string = edgeColor || '#3b82f6'; + if (isMergedTrace && mergedColors && mergedColors.length >= 2) { + const gradientStops = mergedColors.map((color, idx) => + `${color} ${(idx / mergedColors.length) * 100}%, ${color} ${((idx + 1) / mergedColors.length) * 100}%` + ).join(', '); + handleBackground = `linear-gradient(45deg, ${gradientStops})`; + } + return (
- + {i}
); })} + + {/* Prepend input handles for traces that this node is the HEAD of */} + {/* Show dashed handle if no prepend connection yet (can accept prepend) */} + {/* Show solid handle if already has prepend connection (connected) */} + {data.outgoingTraces && data.outgoingTraces + .filter(trace => { + // Check if this is a self trace, fork trace originated from this node + const isSelfTrace = trace.id === `trace-${id}`; + // Strict check for fork trace: must be originated from this node + const isForkTrace = trace.id.startsWith('fork-') && trace.sourceNodeId === id; + + if (!isSelfTrace && !isForkTrace) return false; + + // Check if this trace has any outgoing edges (downstream connections) + const hasDownstream = edges.some(e => + e.source === id && e.sourceHandle === `trace-${trace.id}` + ); + return hasDownstream; + }) + .map((trace) => { + // Check if there's already a prepend connection to this trace + const hasPrependConnection = edges.some(e => + e.target === id && e.targetHandle === `prepend-${trace.id}` + ); + const prependEdge = edges.find(e => + e.target === id && e.targetHandle === `prepend-${trace.id}` + ); + + return ( +
+ +
+ ); + }) + } + + {/* Prepend input handles for merged traces - only show if merged trace has downstream */} + {data.mergedTraces && data.mergedTraces + .filter((merged: MergedTrace) => { + // Check if this merged trace has any outgoing edges + return edges.some(e => + e.source === id && e.sourceHandle === `trace-${merged.id}` + ); + }) + .map((merged: MergedTrace) => { + const connectedEdge = edges.find(e => e.target === id && e.targetHandle === `prepend-${merged.id}`); + const edgeColor = connectedEdge?.style?.stroke as string; + + // Create gradient for merged trace handle + const colors = merged.colors.length > 0 ? merged.colors : ['#888']; + const gradientStops = colors.map((color, idx) => + `${color} ${(idx / colors.length) * 100}%, ${color} ${((idx + 1) / colors.length) * 100}%` + ).join(', '); + const stripeGradient = `linear-gradient(45deg, ${gradientStops})`; + + return ( +
+ +
+ ); + })}
{/* Dynamic Outputs (Traces) */}
- {/* 1. Outgoing Traces (Pass-through + Self) */} - {data.outgoingTraces && data.outgoingTraces.map((trace, i) => ( + {/* 0. Incoming Trace Continue Handles - allow continuing an incoming trace */} + {/* Only show if there's NO downstream edge yet (dashed = waiting for connection) */} + {data.traces && data.traces + .filter((trace: Trace) => { + // Only show continue handle if NOT already connected downstream + const evolvedTraceId = `${trace.id}_${id}`; + const hasOutgoingEdge = edges.some(e => + e.source === id && + (e.sourceHandle === `trace-${trace.id}` || e.sourceHandle === `trace-${evolvedTraceId}`) + ); + // Only show dashed handle if not yet connected + return !hasOutgoingEdge; + }) + .map((trace: Trace) => { + const evolvedTraceId = `${trace.id}_${id}`; + return ( +
+ +
+ ); + })} + + {/* 1. Regular Outgoing Traces (Self + Forks + Connected Pass-through) */} + {/* Only show traces that have actual downstream edges */} + {data.outgoingTraces && data.outgoingTraces + .filter(trace => { + // Exclude merged traces - they have their own section + if (trace.id.startsWith('merged-')) return false; + + // Only show if there's an actual downstream edge using this trace + const hasDownstream = edges.some(e => + e.source === id && e.sourceHandle === `trace-${trace.id}` + ); + return hasDownstream; + }) + .map((trace) => (
) => {
))} - {/* 2. New Branch Generator Handle (Always visible) */} + {/* 2. Merged Trace Handles (with alternating color stripes) */} + {data.mergedTraces && data.mergedTraces.map((merged: MergedTrace) => { + // Create a gradient background from the source trace colors + const colors = merged.colors.length > 0 ? merged.colors : ['#888']; + const gradientStops = colors.map((color, idx) => + `${color} ${(idx / colors.length) * 100}%, ${color} ${((idx + 1) / colors.length) * 100}%` + ).join(', '); + const stripeGradient = `linear-gradient(45deg, ${gradientStops})`; + + return ( +
+ +
+ ); + })} + + {/* 3. New Branch Generator Handle (Always visible) */}
) => { className="!w-3 !h-3 !bg-gray-400 !right-[-6px]" style={{ top: '50%', transform: 'translateY(-50%)' }} /> - + + New
-- cgit v1.2.3