import { useEffect, useState } from 'react'; import { Handle, Position, type NodeProps, useUpdateNodeInternals, useEdges } from 'reactflow'; import type { NodeData, MergedTrace, Trace } from '../../store/flowStore'; import { Loader2, MessageSquare } from 'lucide-react'; import useFlowStore from '../../store/flowStore'; const LLMNode = ({ id, data, selected }: NodeProps) => { const { theme } = 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.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 // plus all currently connected handles. // Find all edges connected to this node's inputs const connectedHandles = new Set( edges .filter(e => e.target === id) .map(e => e.targetHandle) ); // Logic: // If input-0 is connected, show input-1. // If input-1 is connected, show input-2. // We can just iterate until we find an unconnected one. let handleCount = 1; while (connectedHandles.has(`input-${handleCount - 1}`)) { handleCount++; } // But wait, if we delete an edge to input-0, we still want input-1 to exist if it's connected? // No, usually in this designs, we just render up to max(connected_index) + 1. // Let's get the max index connected let maxConnectedIndex = -1; edges.filter(e => e.target === id).forEach(e => { const idx = parseInt(e.targetHandle?.replace('input-', '') || '0'); if (!isNaN(idx) && idx > maxConnectedIndex) { maxConnectedIndex = idx; } }); 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}
{/* 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 - REMOVED per user request */} {/* Users should prepend to parent traces instead */}
{/* Dynamic Outputs (Traces) */}
{/* 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 // Now that trace IDs don't evolve, we check the base ID const hasOutgoingEdge = edges.some(e => e.source === id && e.sourceHandle === `trace-${trace.id}` ); // Only show dashed handle if not yet connected return !hasOutgoingEdge; }) .map((trace: Trace) => { // Handle merged trace visualization let backgroundStyle = trace.color; if (trace.isMerged && trace.mergedColors && trace.mergedColors.length > 0) { const colors = trace.mergedColors; 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})`; } return (
); })} {/* 1. Regular Outgoing Traces (Self + Forks + Connected Pass-through) */} {/* Only show traces that have actual downstream edges */} {data.outgoingTraces && data.outgoingTraces .filter(trace => { // Check if this is a locally created merged trace (should be shown in Part 2) const isLocallyMerged = data.mergedTraces?.some(m => m.id === trace.id); if (isLocallyMerged) 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) => { // Handle merged trace visualization let backgroundStyle = trace.color; if (trace.isMerged && trace.mergedColors && trace.mergedColors.length > 0) { const colors = trace.mergedColors; const gradientStops = colors.map((color, idx) => `${color} ${(idx / colors.length) * 100}%, ${color} ${((idx + 1) / colors.length) * 100}%` ).join(', '); backgroundStyle = `linear-gradient(45deg, ${gradientStops})`; } return (
); })} {/* 2. Merged Trace Handles (with alternating color stripes) */} {data.mergedTraces && data.mergedTraces.map((merged: MergedTrace) => { // Check if this merged trace has any outgoing edges const hasOutgoingEdge = edges.some(e => e.source === id && e.sourceHandle === `trace-${merged.id}` ); // 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) */}
+ New
{data.status === 'error' && (
Error
)}
); }; export default LLMNode;