| 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<StepsListProps> = ({ trace }) => {
|
| const containerRef = useRef<HTMLDivElement>(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<NodeJS.Timeout | null>(null);
|
| const streamStartTimeRef = useRef<number | null>(null);
|
| const [showConnectionCard, setShowConnectionCard] = React.useState(false);
|
| const hasConnectedRef = useRef(false);
|
|
|
|
|
| const isFinalStepActive = selectedStepIndex === null && finalStep && !trace?.isRunning;
|
|
|
|
|
| const isThinkingCardActive = selectedStepIndex === null && showThinkingCard;
|
|
|
|
|
|
|
|
|
|
|
|
|
| const activeStepIndex = selectedStepIndex !== null
|
| ? selectedStepIndex
|
| : isFinalStepActive
|
| ? null
|
| : isThinkingCardActive
|
| ? null
|
| : (trace?.steps && trace.steps.length > 0 && trace?.isRunning)
|
| ? trace.steps.length - 1
|
| : (trace?.steps && trace.steps.length > 0)
|
| ? trace.steps.length - 1
|
| : null;
|
|
|
|
|
|
|
|
|
| useEffect(() => {
|
| if (isConnectingToE2B || isAgentProcessing || (trace?.steps && trace.steps.length > 0) || finalStep) {
|
| setShowConnectionCard(true);
|
| hasConnectedRef.current = true;
|
| }
|
| }, [isConnectingToE2B, isAgentProcessing, trace?.steps, finalStep]);
|
|
|
|
|
|
|
|
|
|
|
| useEffect(() => {
|
|
|
|
|
| if (isAgentProcessing && !isConnectingToE2B && !streamStartTimeRef.current) {
|
| streamStartTimeRef.current = Date.now();
|
| }
|
|
|
|
|
| if (!isAgentProcessing || finalStep) {
|
| streamStartTimeRef.current = null;
|
| setShowThinkingCard(false);
|
| if (thinkingTimeoutRef.current) {
|
| clearTimeout(thinkingTimeoutRef.current);
|
| thinkingTimeoutRef.current = null;
|
| }
|
| return;
|
| }
|
|
|
|
|
| if (isAgentProcessing && !isConnectingToE2B && !finalStep && streamStartTimeRef.current) {
|
|
|
| if (thinkingTimeoutRef.current) {
|
| clearTimeout(thinkingTimeoutRef.current);
|
| }
|
|
|
|
|
| const elapsedTime = Date.now() - streamStartTimeRef.current;
|
| const remainingTime = Math.max(0, 5000 - elapsedTime);
|
|
|
| thinkingTimeoutRef.current = setTimeout(() => {
|
| setShowThinkingCard(true);
|
| }, remainingTime);
|
| }
|
|
|
|
|
| return () => {
|
| if (thinkingTimeoutRef.current) {
|
| clearTimeout(thinkingTimeoutRef.current);
|
| thinkingTimeoutRef.current = null;
|
| }
|
| };
|
| }, [isAgentProcessing, isConnectingToE2B, finalStep]);
|
|
|
|
|
| useEffect(() => {
|
| const container = containerRef.current;
|
| if (!container) return;
|
|
|
| isScrollingProgrammatically.current = true;
|
|
|
|
|
| setTimeout(() => {
|
| if (!container) return;
|
|
|
|
|
| if (selectedStepIndex === null) {
|
|
|
| container.scrollTo({
|
| top: container.scrollHeight,
|
| behavior: 'smooth',
|
| });
|
| }
|
|
|
| else {
|
| const selectedElement = container.querySelector(`[data-step-index="${selectedStepIndex}"]`);
|
| if (selectedElement) {
|
| selectedElement.scrollIntoView({
|
| behavior: 'smooth',
|
| block: 'center',
|
| });
|
| }
|
| }
|
|
|
|
|
| setTimeout(() => {
|
| isScrollingProgrammatically.current = false;
|
| }, 500);
|
| }, 100);
|
| }, [selectedStepIndex, trace?.steps?.length, showThinkingCard, finalStep]);
|
|
|
|
|
| useEffect(() => {
|
| const container = containerRef.current;
|
| if (!container || !trace?.steps || trace.steps.length === 0) return;
|
|
|
| const handleScroll = () => {
|
|
|
| if (isScrollingProgrammatically.current) return;
|
|
|
|
|
| if (trace?.isRunning) return;
|
|
|
| const containerRect = container.getBoundingClientRect();
|
| const containerTop = containerRect.top;
|
| const containerBottom = containerRect.bottom;
|
| const containerCenter = containerRect.top + containerRect.height / 2;
|
|
|
|
|
| const isAtTop = container.scrollTop <= 5;
|
| const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 5;
|
|
|
| let targetStepIndex: number | null = -1;
|
| let targetDistance = Infinity;
|
| let isFinalStepTarget = false;
|
|
|
| if (isAtTop) {
|
|
|
| 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) {
|
|
|
| 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;
|
| }
|
| }
|
| });
|
|
|
|
|
| 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 {
|
|
|
| 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;
|
| }
|
| }
|
| });
|
|
|
|
|
| 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;
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
| if (isFinalStepTarget && selectedStepIndex !== null) {
|
| setSelectedStepIndex(null);
|
| } else if (!isFinalStepTarget && targetStepIndex !== -1 && targetStepIndex !== selectedStepIndex) {
|
| setSelectedStepIndex(targetStepIndex);
|
| }
|
| };
|
|
|
|
|
| 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 (
|
| <Paper
|
| elevation={0}
|
| sx={{
|
| width: { xs: '100%', md: 320 },
|
| flexShrink: 0,
|
| display: 'flex',
|
| flexDirection: 'column',
|
| ml: { xs: 0, md: 1.5 },
|
| mt: { xs: 3, md: 0 },
|
| overflow: 'hidden',
|
| }}
|
| >
|
| <Box sx={{ px: 2, py: 1.5, borderBottom: '1px solid', borderColor: 'divider', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
| <Typography variant="h6" sx={{ fontSize: '0.9rem', fontWeight: 700, color: 'text.primary' }}>
|
| Steps
|
| </Typography>
|
| {trace?.traceMetadata && trace.traceMetadata.numberOfSteps > 0 && (
|
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0 }}>
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| color: 'text.primary',
|
| }}
|
| >
|
| {trace.traceMetadata.numberOfSteps}
|
| </Typography>
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| color: 'text.disabled',
|
| }}
|
| >
|
| /{trace.traceMetadata.maxSteps}
|
| </Typography>
|
| </Box>
|
| )}
|
| </Box>
|
| <Box
|
| ref={containerRef}
|
| sx={{
|
| flex: 1,
|
| overflowY: 'auto',
|
| minHeight: 0,
|
| p: 2,
|
| }}
|
| >
|
| {(trace?.steps && trace.steps.length > 0) || finalStep || showThinkingCard || showConnectionCard ? (
|
| <Stack spacing={2.5}>
|
| {/* Show connection step card (first item) */}
|
| {showConnectionCard && (
|
| <Box data-step-index="connection">
|
| <ConnectionStepCard isConnecting={isConnectingToE2B} />
|
| </Box>
|
| )}
|
|
|
| {/* Show all steps */}
|
| {trace?.steps && trace.steps.map((step, index) => (
|
| <Box key={step.stepId} data-step-index={index}>
|
| <StepCard
|
| step={step}
|
| index={index}
|
| isLatest={index === trace.steps!.length - 1}
|
| isActive={index === activeStepIndex}
|
| />
|
| </Box>
|
| ))}
|
|
|
| {/* Show thinking indicator after steps (appears 5 seconds after stream start) */}
|
| {showThinkingCard && (
|
| <Box data-step-index="thinking">
|
| <ThinkingStepCard isActive={isThinkingCardActive} />
|
| </Box>
|
| )}
|
|
|
| {/* Show final step card if exists */}
|
| {finalStep && (
|
| <Box data-step-index="final">
|
| <FinalStepCard
|
| finalStep={finalStep}
|
| isActive={isFinalStepActive}
|
| />
|
| </Box>
|
| )}
|
| </Stack>
|
| ) : (
|
| <Box
|
| sx={{
|
| display: 'flex',
|
| flexDirection: 'column',
|
| alignItems: 'center',
|
| justifyContent: 'center',
|
| height: '100%',
|
| color: 'text.secondary',
|
| p: 3,
|
| textAlign: 'center',
|
| }}
|
| >
|
| <ListAltIcon sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
|
| <Typography variant="body1" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| No steps yet
|
| </Typography>
|
| <Typography variant="caption" sx={{ fontSize: '0.75rem' }}>
|
| Steps will appear as the agent progresses
|
| </Typography>
|
| </Box>
|
| )}
|
| </Box>
|
| </Paper>
|
| );
|
| };
|
|
|