From c3673766aecdb988bb4e811376d4f1f1e18f1e0f Mon Sep 17 00:00:00 2001 From: blackhao <13851610112@163.com> Date: Tue, 9 Dec 2025 15:05:04 -0600 Subject: some fix on trace merging --- frontend/src/components/Sidebar.tsx | 240 +++++++++++++++--------- frontend/src/components/nodes/LLMNode.tsx | 74 ++++---- frontend/src/store/flowStore.ts | 298 ++++++++++++++++++++++++------ 3 files changed, 432 insertions(+), 180 deletions(-) (limited to 'frontend/src') diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 5516629..06c8704 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -544,56 +544,118 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { return true; }; - // Helper: Check if specific trace path upstream has complete nodes - const checkTracePathComplete = (nodeId: string, traceId: string, visited: Set = new Set()): boolean => { - if (visited.has(nodeId)) return true; - visited.add(nodeId); - - // Find the incoming edge that carries this trace - const incomingEdge = edges.find(e => - e.target === nodeId && - e.sourceHandle === `trace-${traceId}` - ); - - if (!incomingEdge) return true; // Reached head of trace segment - + // Helper: find incoming edge for a given trace ID (with fallbacks) + const findIncomingEdgeForTrace = (nodeId: string, traceId: string): Edge | null => { + // 1) exact match by sourceHandle + let edge = edges.find(e => e.target === nodeId && e.sourceHandle === `trace-${traceId}`); + if (edge) return edge; + // 2) fallback: any incoming edge whose source has this trace in outgoingTraces + edge = edges.find(e => { + if (e.target !== nodeId) return false; + const src = nodes.find(n => n.id === e.source); + return src?.data.outgoingTraces?.some((t: Trace) => t.id === traceId); + }); + return edge || null; + }; + + // Helper: get source trace IDs for a merged trace on a given node (supports propagated merged traces) + const getMergedSourceIds = (nodeId: string, traceId: string): string[] => { + const node = nodes.find(n => n.id === nodeId); + if (!node) return []; + const mergedLocal = node.data.mergedTraces?.find((m: MergedTrace) => m.id === traceId); + if (mergedLocal) return mergedLocal.sourceTraceIds || []; + const incomingMatch = node.data.traces?.find((t: Trace) => t.id === traceId); + if (incomingMatch?.isMerged && incomingMatch.sourceTraceIds) return incomingMatch.sourceTraceIds; + const outgoingMatch = node.data.outgoingTraces?.find((t: Trace) => t.id === traceId); + if (outgoingMatch?.isMerged && outgoingMatch.sourceTraceIds) return outgoingMatch.sourceTraceIds; + return []; + }; + + // Recursive: Check if specific trace path upstream has complete nodes (supports multi-level merged) + const checkTracePathComplete = ( + nodeId: string, + traceId: string, + visited: Set = new Set() + ): boolean => { + const visitKey = `${nodeId}-${traceId}`; + if (visited.has(visitKey)) return true; + visited.add(visitKey); + + // Determine if this node is the merge owner or just receiving a propagated merged trace + const localMerge = nodes.find(n => n.id === nodeId)?.data.mergedTraces?.some(m => m.id === traceId); + const localParents = getMergedSourceIds(nodeId, traceId); + + const incomingEdge = findIncomingEdgeForTrace(nodeId, traceId); + if (!incomingEdge) { + // If no incoming edge and this node owns the merge, check parents from here + if (localMerge && localParents.length > 0) { + for (const pid of localParents) { + if (!checkTracePathComplete(nodeId, pid, visited)) return false; + } + return true; + } + return true; // head + } + const sourceNode = nodes.find(n => n.id === incomingEdge.source); if (!sourceNode || sourceNode.data.disabled) return true; - - // Check if source node is complete - if (!sourceNode.data.userPrompt || !sourceNode.data.response) { - return false; // Found incomplete node + + // If merged at sourceNode (or propagated merged), recurse into each parent from the merge owner + const parentIds = localMerge ? localParents : getMergedSourceIds(sourceNode.id, traceId); + if (parentIds.length > 0) { + const mergeOwnerId = localMerge ? nodeId : sourceNode.id; + for (const pid of parentIds) { + if (!checkTracePathComplete(mergeOwnerId, pid, visited)) return false; + } + return true; } - - // Continue upstream + + // Regular trace: check node content then continue upstream + if (!sourceNode.data.userPrompt || !sourceNode.data.response) return false; return checkTracePathComplete(sourceNode.id, traceId, visited); }; - // Helper: Find the first empty node on a specific trace path - const findEmptyNodeOnTrace = (nodeId: string, traceId: string, visited: Set = new Set()): string | null => { - if (visited.has(nodeId)) return null; - visited.add(nodeId); - - const incomingEdge = edges.find(e => - e.target === nodeId && - e.sourceHandle === `trace-${traceId}` - ); - - if (!incomingEdge) return null; - + // Recursive: Find the first empty node on a specific trace path (supports multi-level merged) + const findEmptyNodeOnTrace = ( + nodeId: string, + traceId: string, + visited: Set = new Set() + ): string | null => { + const visitKey = `${nodeId}-${traceId}`; + if (visited.has(visitKey)) return null; + visited.add(visitKey); + + // Determine if this node owns the merge or just receives propagated merged trace + const localMerge = nodes.find(n => n.id === nodeId)?.data.mergedTraces?.some(m => m.id === traceId); + const localParents = getMergedSourceIds(nodeId, traceId); + + const incomingEdge = findIncomingEdgeForTrace(nodeId, traceId); + if (!incomingEdge) { + if (localMerge && localParents.length > 0) { + for (const pid of localParents) { + const upstreamEmpty = findEmptyNodeOnTrace(nodeId, pid, visited); + if (upstreamEmpty) return upstreamEmpty; + } + } + return null; + } + const sourceNode = nodes.find(n => n.id === incomingEdge.source); if (!sourceNode || sourceNode.data.disabled) return null; - - // Recursively check upstream first (find the furthest empty node) - const upstreamEmpty = findEmptyNodeOnTrace(sourceNode.id, traceId, visited); - if (upstreamEmpty) return upstreamEmpty; - - // If no further upstream empty, check this node + + const parentIds = localMerge ? localParents : getMergedSourceIds(sourceNode.id, traceId); + if (parentIds.length > 0) { + const mergeOwnerId = localMerge ? nodeId : sourceNode.id; + for (const pid of parentIds) { + const upstreamEmpty = findEmptyNodeOnTrace(mergeOwnerId, pid, visited); + if (upstreamEmpty) return upstreamEmpty; + } + } + if (!sourceNode.data.userPrompt || !sourceNode.data.response) { return sourceNode.id; } - - return null; + return findEmptyNodeOnTrace(sourceNode.id, traceId, visited); }; // Check if all active traces are complete (for main Run Node button) @@ -603,23 +665,10 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { const activeTraceIds = selectedNode.data.activeTraceIds || []; if (activeTraceIds.length === 0) return { complete: true }; - // Check upstream nodes ONLY for active traces + // Check upstream nodes ONLY for active traces (supports merged trace recursion) for (const traceId of activeTraceIds) { - // Check if it's a merged trace - const merged = selectedNode.data.mergedTraces?.find((m: MergedTrace) => m.id === traceId); - - if (merged) { - // For merged trace, check all source traces - for (const sourceId of merged.sourceTraceIds) { - if (!checkTracePathComplete(selectedNode.id, sourceId)) { - return { complete: false, incompleteTraceId: 'upstream' }; - } - } - } else { - // For regular trace - if (!checkTracePathComplete(selectedNode.id, traceId)) { - return { complete: false, incompleteTraceId: 'upstream' }; - } + if (!checkTracePathComplete(selectedNode.id, traceId)) { + return { complete: false, incompleteTraceId: 'upstream' }; } } @@ -632,12 +681,11 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { } } - // Check merged traces content - const mergedTraces = selectedNode.data.mergedTraces || []; + // Check merged traces content (including propagated merged traces) for (const traceId of activeTraceIds) { - const merged = mergedTraces.find((m: MergedTrace) => m.id === traceId); - if (merged) { - for (const sourceId of merged.sourceTraceIds) { + const sourceIds = getMergedSourceIds(selectedNode.id, traceId); + if (sourceIds.length > 0) { + for (const sourceId of sourceIds) { const sourceTrace = incomingTraces.find((t: Trace) => t.id === sourceId); if (sourceTrace && !isTraceComplete(sourceTrace)) { return { complete: false, incompleteTraceId: sourceId }; @@ -655,18 +703,7 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { const activeTraceIds = selectedNode.data.activeTraceIds || []; for (const traceId of activeTraceIds) { - let emptyNodeId: string | null = null; - - const merged = selectedNode.data.mergedTraces?.find((m: MergedTrace) => m.id === traceId); - if (merged) { - for (const sourceId of merged.sourceTraceIds) { - emptyNodeId = findEmptyNodeOnTrace(selectedNode.id, sourceId); - if (emptyNodeId) break; - } - } else { - emptyNodeId = findEmptyNodeOnTrace(selectedNode.id, traceId); - } - + const emptyNodeId = findEmptyNodeOnTrace(selectedNode.id, traceId); if (emptyNodeId) { const emptyNode = nodes.find(n => n.id === emptyNodeId); if (emptyNode) { @@ -1237,8 +1274,32 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => {
-
- #{trace.id.slice(-4)} + {trace.isMerged ? ( + // Merged Trace Rendering (for propagated merged traces) +
+ {(trace.mergedColors || [trace.color]).slice(0, 3).map((color, idx) => ( +
+ ))} + {(trace.mergedColors?.length || 0) > 3 && ( +
+ + +
+ )} +
+ ) : ( + // Regular Trace Rendering +
+ )} + + + {trace.isMerged ? 'Merged ' : ''}#{trace.id.slice(-4)} + {!isComplete && ( (incomplete) )} @@ -1359,10 +1420,13 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { e.stopPropagation(); openMergedQuickChat(merged); }} + disabled={!isComplete} className={`p-1 rounded shrink-0 ${ - isDark ? 'hover:bg-purple-900 text-gray-500 hover:text-purple-400' : 'hover:bg-purple-100 text-gray-400 hover:text-purple-600' + isComplete + ? isDark ? 'hover:bg-purple-900 text-gray-500 hover:text-purple-400' : 'hover:bg-purple-100 text-gray-400 hover:text-purple-600' + : 'text-gray-500 cursor-not-allowed opacity-50' }`} - title="Quick Chat with merged context" + title={isComplete ? "Quick Chat with merged context" : "Trace incomplete"} > @@ -1473,12 +1537,18 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => {