|
|
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>
|
|
|
);
|
|
|
};
|
|
|
|