import React, { useRef, useEffect } from 'react'; import { AgentTrace } from '@/types/agent'; import { Box, Typography, Stack, Paper } from '@mui/material'; import { StepCard } from './StepCard'; import { FinalStepCard } from './FinalStepCard'; import { ThinkingStepCard } from './ThinkingStepCard'; import { ConnectionStepCard } from './ConnectionStepCard'; import ListAltIcon from '@mui/icons-material/ListAlt'; import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore'; interface StepsListProps { trace?: AgentTrace; } export const StepsList: React.FC = ({ trace }) => { const containerRef = useRef(null); const selectedStepIndex = useAgentStore(selectSelectedStepIndex); const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); const finalStep = useAgentStore(selectFinalStep); const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B); const isAgentProcessing = useAgentStore(selectIsAgentProcessing); const isScrollingProgrammatically = useRef(false); const [showThinkingCard, setShowThinkingCard] = React.useState(false); const thinkingTimeoutRef = useRef(null); const streamStartTimeRef = useRef(null); const [showConnectionCard, setShowConnectionCard] = React.useState(false); const hasConnectedRef = useRef(false); // Check if final step is active (when selectedStepIndex is null and finalStep exists and trace is not running) const isFinalStepActive = selectedStepIndex === null && finalStep && !trace?.isRunning; // Check if thinking card is active (when in live mode and thinking card is shown) const isThinkingCardActive = selectedStepIndex === null && showThinkingCard; // Determine the active step index // If a specific step is selected, use that // If the final step is active, no normal step should be active // If the thinking card is active, no normal step should be active // Otherwise, show the last step as active const activeStepIndex = selectedStepIndex !== null ? selectedStepIndex : isFinalStepActive ? null // When final step is active, no normal step is active : isThinkingCardActive ? null // When thinking card is active, no normal step is active : (trace?.steps && trace.steps.length > 0 && trace?.isRunning) ? trace.steps.length - 1 : (trace?.steps && trace.steps.length > 0) ? trace.steps.length - 1 : null; // Manage ConnectionStepCard display: // - Shows when isConnectingToE2B = true OR when we had a connection // - Remains visible even when task is finished (if we have steps or finalStep) useEffect(() => { if (isConnectingToE2B || isAgentProcessing || (trace?.steps && trace.steps.length > 0) || finalStep) { setShowConnectionCard(true); hasConnectedRef.current = true; } }, [isConnectingToE2B, isAgentProcessing, trace?.steps, finalStep]); // Manage ThinkingCard display: // - Appears 5 seconds AFTER stream starts (isAgentProcessing = true, NOT during isConnectingToE2B) // - Remains visible during the entire agent processing // - Hides only when agent stops OR a finalStep exists useEffect(() => { // If stream really starts (isAgentProcessing = true and NOT connecting) // And no startTime recorded yet if (isAgentProcessing && !isConnectingToE2B && !streamStartTimeRef.current) { streamStartTimeRef.current = Date.now(); } // If agent stops OR we have a finalStep, reset and hide if (!isAgentProcessing || finalStep) { streamStartTimeRef.current = null; setShowThinkingCard(false); if (thinkingTimeoutRef.current) { clearTimeout(thinkingTimeoutRef.current); thinkingTimeoutRef.current = null; } return; } // If agent is running, not connecting, no finalStep: start 5 second timer if (isAgentProcessing && !isConnectingToE2B && !finalStep && streamStartTimeRef.current) { // Clean up any existing timeout if (thinkingTimeoutRef.current) { clearTimeout(thinkingTimeoutRef.current); } // Calculate elapsed time since stream started const elapsedTime = Date.now() - streamStartTimeRef.current; const remainingTime = Math.max(0, 5000 - elapsedTime); thinkingTimeoutRef.current = setTimeout(() => { setShowThinkingCard(true); }, remainingTime); } // Cleanup on unmount or when dependencies change return () => { if (thinkingTimeoutRef.current) { clearTimeout(thinkingTimeoutRef.current); thinkingTimeoutRef.current = null; } }; }, [isAgentProcessing, isConnectingToE2B, finalStep]); // Auto-scroll logic useEffect(() => { const container = containerRef.current; if (!container) return; isScrollingProgrammatically.current = true; // Use setTimeout to ensure DOM has updated setTimeout(() => { if (!container) return; // LIVE MODE: Always scroll to the bottom (last visible element) if (selectedStepIndex === null) { // Scroll to bottom container.scrollTo({ top: container.scrollHeight, behavior: 'smooth', }); } // NON-LIVE MODE: Scroll to selected step else { const selectedElement = container.querySelector(`[data-step-index="${selectedStepIndex}"]`); if (selectedElement) { selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center', }); } } // Reset flag after scroll animation setTimeout(() => { isScrollingProgrammatically.current = false; }, 500); }, 100); }, [selectedStepIndex, trace?.steps?.length, showThinkingCard, finalStep]); // Detect which step is visible when scrolling (steps → timeline) useEffect(() => { const container = containerRef.current; if (!container || !trace?.steps || trace.steps.length === 0) return; const handleScroll = () => { // Don't update if we're scrolling programmatically if (isScrollingProgrammatically.current) return; // Don't update if agent is running (stay in live mode) if (trace?.isRunning) return; const containerRect = container.getBoundingClientRect(); const containerTop = containerRect.top; const containerBottom = containerRect.bottom; const containerCenter = containerRect.top + containerRect.height / 2; // Check scroll position const isAtTop = container.scrollTop <= 5; // 5px tolerance const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 5; // 5px tolerance let targetStepIndex: number | null = -1; let targetDistance = Infinity; let isFinalStepTarget = false; if (isAtTop) { // At the top: find the highest visible step let highestVisibleBottom = Infinity; trace.steps.forEach((_, index) => { const stepElement = container.querySelector(`[data-step-index="${index}"]`); if (stepElement) { const stepRect = stepElement.getBoundingClientRect(); const stepTop = stepRect.top; const stepBottom = stepRect.bottom; const isVisible = stepTop < containerBottom && stepBottom > containerTop; if (isVisible && stepTop < highestVisibleBottom) { highestVisibleBottom = stepTop; targetStepIndex = index; isFinalStepTarget = false; } } }); } else if (isAtBottom) { // At the bottom: find the lowest visible step let lowestVisibleTop = -Infinity; trace.steps.forEach((_, index) => { const stepElement = container.querySelector(`[data-step-index="${index}"]`); if (stepElement) { const stepRect = stepElement.getBoundingClientRect(); const stepTop = stepRect.top; const stepBottom = stepRect.bottom; const isVisible = stepTop < containerBottom && stepBottom > containerTop; if (isVisible && stepTop > lowestVisibleTop) { lowestVisibleTop = stepTop; targetStepIndex = index; isFinalStepTarget = false; } } }); // Check if final step is the lowest visible if (finalStep) { const finalStepElement = container.querySelector(`[data-step-index="final"]`); if (finalStepElement) { const finalStepRect = finalStepElement.getBoundingClientRect(); const finalStepTop = finalStepRect.top; const finalStepBottom = finalStepRect.bottom; const isVisible = finalStepTop < containerBottom && finalStepBottom > containerTop; if (isVisible && finalStepTop > lowestVisibleTop) { targetStepIndex = null; isFinalStepTarget = true; } } } } else { // Not at bottom: find the step closest to center trace.steps.forEach((_, index) => { const stepElement = container.querySelector(`[data-step-index="${index}"]`); if (stepElement) { const stepRect = stepElement.getBoundingClientRect(); const stepCenter = stepRect.top + stepRect.height / 2; const distance = Math.abs(containerCenter - stepCenter); if (distance < targetDistance) { targetDistance = distance; targetStepIndex = index; isFinalStepTarget = false; } } }); // Check if final step is closest to center if (finalStep) { const finalStepElement = container.querySelector(`[data-step-index="final"]`); if (finalStepElement) { const finalStepRect = finalStepElement.getBoundingClientRect(); const finalStepCenter = finalStepRect.top + finalStepRect.height / 2; const distance = Math.abs(containerCenter - finalStepCenter); if (distance < targetDistance) { targetStepIndex = null; isFinalStepTarget = true; } } } } // Update the selected step if changed if (isFinalStepTarget && selectedStepIndex !== null) { setSelectedStepIndex(null); } else if (!isFinalStepTarget && targetStepIndex !== -1 && targetStepIndex !== selectedStepIndex) { setSelectedStepIndex(targetStepIndex); } }; // Throttle scroll events let scrollTimeout: NodeJS.Timeout; const throttledScroll = () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(handleScroll, 150); }; container.addEventListener('scroll', throttledScroll); return () => { container.removeEventListener('scroll', throttledScroll); clearTimeout(scrollTimeout); }; }, [trace?.steps, selectedStepIndex, setSelectedStepIndex, finalStep]); return ( Steps {trace?.traceMetadata && trace.traceMetadata.numberOfSteps > 0 && ( {trace.traceMetadata.numberOfSteps} /{trace.traceMetadata.maxSteps} )} {(trace?.steps && trace.steps.length > 0) || finalStep || showThinkingCard || showConnectionCard ? ( {/* Show connection step card (first item) */} {showConnectionCard && ( )} {/* Show all steps */} {trace?.steps && trace.steps.map((step, index) => ( ))} {/* Show thinking indicator after steps (appears 5 seconds after stream start) */} {showThinkingCard && ( )} {/* Show final step card if exists */} {finalStep && ( )} ) : ( No steps yet Steps will appear as the agent progresses )} ); };