|
|
import React, { useRef, useEffect } from 'react';
|
|
|
import { Box, Typography, CircularProgress, Button } from '@mui/material';
|
|
|
import CheckIcon from '@mui/icons-material/Check';
|
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|
|
import StopCircleIcon from '@mui/icons-material/StopCircle';
|
|
|
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
|
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
|
|
import CableIcon from '@mui/icons-material/Cable';
|
|
|
import { AgentTraceMetadata } from '@/types/agent';
|
|
|
import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore';
|
|
|
|
|
|
interface TimelineProps {
|
|
|
metadata: AgentTraceMetadata;
|
|
|
isRunning: boolean;
|
|
|
}
|
|
|
|
|
|
export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
|
const timelineRef = 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 showConnectionIndicator = isConnectingToE2B || isAgentProcessing || (metadata.numberOfSteps > 0) || finalStep;
|
|
|
|
|
|
|
|
|
|
|
|
const totalStepsToShow = isRunning && !isConnectingToE2B
|
|
|
? metadata.numberOfSteps + 1
|
|
|
: metadata.numberOfSteps;
|
|
|
|
|
|
|
|
|
const lineWidth = finalStep
|
|
|
? `calc(${totalStepsToShow} * (40px + 12px) + 52px)`
|
|
|
: `calc(${totalStepsToShow} * (40px + 12px))`;
|
|
|
|
|
|
const steps = Array.from({ length: totalStepsToShow }, (_, index) => ({
|
|
|
stepNumber: index + 1,
|
|
|
stepIndex: index,
|
|
|
isCompleted: index < metadata.numberOfSteps,
|
|
|
|
|
|
isCurrent: (index === metadata.numberOfSteps && isRunning && !isConnectingToE2B) ||
|
|
|
(index === 0 && metadata.numberOfSteps === 0 && isRunning && !isConnectingToE2B),
|
|
|
isSelected: selectedStepIndex === index,
|
|
|
}));
|
|
|
|
|
|
|
|
|
const handleStepClick = (stepIndex: number, isCompleted: boolean, isCurrent: boolean) => {
|
|
|
if (isCompleted) {
|
|
|
setSelectedStepIndex(stepIndex);
|
|
|
} else if (isCurrent) {
|
|
|
|
|
|
setSelectedStepIndex(null);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
const handleFinalStepClick = () => {
|
|
|
setSelectedStepIndex(null);
|
|
|
};
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (timelineRef.current && isRunning) {
|
|
|
|
|
|
const currentStepElement = timelineRef.current.querySelector(`[data-step="${metadata.numberOfSteps}"]`);
|
|
|
if (currentStepElement) {
|
|
|
currentStepElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
|
}
|
|
|
}
|
|
|
}, [metadata.numberOfSteps, isRunning]);
|
|
|
|
|
|
return (
|
|
|
<Box
|
|
|
sx={{
|
|
|
p: 2,
|
|
|
border: '1px solid',
|
|
|
borderColor: 'divider',
|
|
|
borderRadius: '12px',
|
|
|
backgroundColor: 'background.paper',
|
|
|
flexShrink: 0,
|
|
|
}}
|
|
|
>
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
|
{/* Header with step count */}
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
|
<Typography variant="h6" sx={{ fontSize: '0.9rem', fontWeight: 700, color: 'text.primary' }}>
|
|
|
Timeline
|
|
|
{selectedStepIndex !== null && (
|
|
|
<Typography component="span" sx={{ ml: 1, color: 'text.secondary', fontWeight: 500, fontSize: '0.65rem' }}>
|
|
|
- Viewing step {selectedStepIndex + 1}
|
|
|
</Typography>
|
|
|
)}
|
|
|
</Typography>
|
|
|
{selectedStepIndex !== null && (
|
|
|
<Button
|
|
|
size="small"
|
|
|
variant="outlined"
|
|
|
onClick={handleFinalStepClick}
|
|
|
sx={{
|
|
|
textTransform: 'none',
|
|
|
fontSize: '0.7rem',
|
|
|
fontWeight: 600,
|
|
|
px: 1.5,
|
|
|
py: 0.25,
|
|
|
minWidth: 'auto',
|
|
|
color: 'text.secondary',
|
|
|
borderColor: 'divider',
|
|
|
'&:hover': {
|
|
|
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
|
|
|
borderColor: 'text.secondary',
|
|
|
},
|
|
|
}}
|
|
|
>
|
|
|
Back to latest step
|
|
|
</Button>
|
|
|
)}
|
|
|
</Box>
|
|
|
|
|
|
{/* Horizontal scrollable step indicators */}
|
|
|
<Box
|
|
|
ref={timelineRef}
|
|
|
sx={{
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
overflowX: 'auto',
|
|
|
overflowY: 'hidden',
|
|
|
gap: 1.5,
|
|
|
py: 1.5,
|
|
|
height: 60,
|
|
|
position: 'relative',
|
|
|
// Hide scrollbar completely
|
|
|
scrollbarWidth: 'none', // Firefox
|
|
|
'&::-webkit-scrollbar': {
|
|
|
display: 'none', // Chrome, Safari, Edge
|
|
|
},
|
|
|
// Horizontal line crossing through circles
|
|
|
'&::before': {
|
|
|
content: '""',
|
|
|
position: 'absolute',
|
|
|
left: "25px",
|
|
|
// Calculate width to cover visible steps + finalStep if present
|
|
|
width: lineWidth,
|
|
|
top: '19.5px',
|
|
|
transform: 'translateY(-50%)',
|
|
|
transition: 'width 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
|
height: '2px',
|
|
|
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.3)',
|
|
|
zIndex: 0,
|
|
|
pointerEvents: 'none',
|
|
|
},
|
|
|
}}
|
|
|
>
|
|
|
{/* Connection indicator (step 0) */}
|
|
|
{showConnectionIndicator && (
|
|
|
<Box
|
|
|
data-step="connection"
|
|
|
sx={{
|
|
|
display: 'flex',
|
|
|
flexDirection: 'column',
|
|
|
alignItems: 'center',
|
|
|
gap: 0.75,
|
|
|
minWidth: 40,
|
|
|
flexShrink: 0,
|
|
|
position: 'relative',
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
>
|
|
|
{/* White circle background to hide the line */}
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'relative',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
height: 28,
|
|
|
width: 28,
|
|
|
}}
|
|
|
>
|
|
|
{/* White background to hide the line */}
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'absolute',
|
|
|
width: 28,
|
|
|
height: 28,
|
|
|
borderRadius: '50%',
|
|
|
backgroundColor: 'background.paper',
|
|
|
zIndex: 0,
|
|
|
}}
|
|
|
/>
|
|
|
|
|
|
{/* Connection icon */}
|
|
|
{isConnectingToE2B ? (
|
|
|
<CircularProgress
|
|
|
size={20}
|
|
|
thickness={5}
|
|
|
sx={{
|
|
|
color: 'primary.main',
|
|
|
position: 'relative',
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
/>
|
|
|
) : (
|
|
|
<CableIcon
|
|
|
sx={{
|
|
|
fontSize: 20,
|
|
|
color: 'success.main',
|
|
|
position: 'relative',
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
/>
|
|
|
)}
|
|
|
</Box>
|
|
|
|
|
|
{/* Connection label */}
|
|
|
<Typography
|
|
|
variant="caption"
|
|
|
sx={{
|
|
|
fontSize: '0.7rem',
|
|
|
fontWeight: 700,
|
|
|
color: isConnectingToE2B ? 'primary.main' : 'success.main',
|
|
|
whiteSpace: 'nowrap',
|
|
|
}}
|
|
|
>
|
|
|
{isConnectingToE2B ? 'Connecting' : 'Connected'}
|
|
|
</Typography>
|
|
|
</Box>
|
|
|
)}
|
|
|
|
|
|
{/* Render steps and insert final step at the right position */}
|
|
|
{steps.map((step, index) => (
|
|
|
<React.Fragment key={step.stepNumber}>
|
|
|
<Box
|
|
|
data-step={step.stepNumber}
|
|
|
onClick={() => handleStepClick(step.stepIndex, step.isCompleted, step.isCurrent)}
|
|
|
sx={{
|
|
|
display: 'flex',
|
|
|
flexDirection: 'column',
|
|
|
alignItems: 'center',
|
|
|
gap: 0.75,
|
|
|
minWidth: 40,
|
|
|
flexShrink: 0,
|
|
|
position: 'relative',
|
|
|
zIndex: 1,
|
|
|
cursor: (step.isCompleted || step.isCurrent) ? 'pointer' : 'default',
|
|
|
'&:hover': (step.isCompleted || step.isCurrent) ? {
|
|
|
'& .step-dot': {
|
|
|
transform: 'scale(1.15)',
|
|
|
},
|
|
|
} : {},
|
|
|
}}
|
|
|
>
|
|
|
{/* White circle background to hide the line */}
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'relative',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
height: 28,
|
|
|
width: 28,
|
|
|
}}
|
|
|
>
|
|
|
{/* White background to hide the line */}
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'absolute',
|
|
|
width: 28,
|
|
|
height: 28,
|
|
|
borderRadius: '50%',
|
|
|
backgroundColor: 'background.paper',
|
|
|
zIndex: 0,
|
|
|
}}
|
|
|
/>
|
|
|
|
|
|
{/* Step dot */}
|
|
|
{step.isCurrent ? (
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'relative',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
>
|
|
|
<CircularProgress
|
|
|
size={20}
|
|
|
thickness={5}
|
|
|
sx={{
|
|
|
color: 'primary.main',
|
|
|
position: 'absolute',
|
|
|
}}
|
|
|
/>
|
|
|
<Box
|
|
|
sx={{
|
|
|
width: 8,
|
|
|
height: 8,
|
|
|
borderRadius: '50%',
|
|
|
backgroundColor: 'white',
|
|
|
position: 'absolute',
|
|
|
pointerEvents: 'none',
|
|
|
boxShadow: '0 0 4px rgba(0,0,0,0.2)',
|
|
|
}}
|
|
|
/>
|
|
|
</Box>
|
|
|
) : (
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'relative',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
>
|
|
|
<Box
|
|
|
className="step-dot"
|
|
|
sx={{
|
|
|
width: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
|
|
|
height: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
|
|
|
borderRadius: '50%',
|
|
|
// Always keep steps in primary color (blue)
|
|
|
backgroundColor: step.isCompleted
|
|
|
? 'primary.main' // Blue for completed steps
|
|
|
: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.300', // Light grey for future steps
|
|
|
transition: 'all 0.2s ease',
|
|
|
boxShadow: step.isCompleted || step.isSelected
|
|
|
? step.isSelected
|
|
|
? '0 0 8px rgba(255, 167, 38, 0.5)'
|
|
|
: '0 2px 4px rgba(0,0,0,0.1)'
|
|
|
: 'none',
|
|
|
}}
|
|
|
/>
|
|
|
{/* White dot for selected step */}
|
|
|
{step.isSelected && (
|
|
|
<Box
|
|
|
sx={{
|
|
|
width: 8,
|
|
|
height: 8,
|
|
|
borderRadius: '50%',
|
|
|
backgroundColor: 'white',
|
|
|
position: 'absolute',
|
|
|
}}
|
|
|
/>
|
|
|
)}
|
|
|
</Box>
|
|
|
)}
|
|
|
</Box>
|
|
|
|
|
|
{/* Step number - show for all steps */}
|
|
|
<Typography
|
|
|
variant="caption"
|
|
|
sx={{
|
|
|
fontSize: '0.7rem',
|
|
|
fontWeight: step.isSelected || step.isCurrent ? 900 : 400,
|
|
|
color: step.isCurrent
|
|
|
? 'primary.main'
|
|
|
: (step.isCompleted || step.isSelected
|
|
|
? 'text.primary'
|
|
|
: (theme) => theme.palette.mode === 'dark' ? 'grey.700' : 'grey.400'),
|
|
|
whiteSpace: 'nowrap',
|
|
|
lineHeight: 1,
|
|
|
}}
|
|
|
>
|
|
|
{step.stepNumber}
|
|
|
</Typography>
|
|
|
</Box>
|
|
|
|
|
|
{/* Insert final step indicator right after the last completed step */}
|
|
|
{finalStep && step.stepNumber === metadata.numberOfSteps && (
|
|
|
<Box
|
|
|
data-step="final"
|
|
|
onClick={handleFinalStepClick}
|
|
|
sx={{
|
|
|
display: 'flex',
|
|
|
flexDirection: 'column',
|
|
|
alignItems: 'center',
|
|
|
gap: 0.75,
|
|
|
minWidth: 40,
|
|
|
flexShrink: 0,
|
|
|
position: 'relative',
|
|
|
zIndex: 1,
|
|
|
cursor: 'pointer',
|
|
|
'&:hover': {
|
|
|
'& .final-step-icon': {
|
|
|
transform: 'scale(1.15)',
|
|
|
},
|
|
|
},
|
|
|
}}
|
|
|
>
|
|
|
{/* White circle background to hide the line */}
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'relative',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
height: 28,
|
|
|
width: 28,
|
|
|
}}
|
|
|
>
|
|
|
{/* White background to hide the line */}
|
|
|
<Box
|
|
|
sx={{
|
|
|
position: 'absolute',
|
|
|
width: 28,
|
|
|
height: 28,
|
|
|
borderRadius: '50%',
|
|
|
backgroundColor: 'background.paper',
|
|
|
zIndex: 0,
|
|
|
}}
|
|
|
/>
|
|
|
|
|
|
{/* Final step icon */}
|
|
|
<Box
|
|
|
className="final-step-icon"
|
|
|
sx={{
|
|
|
width: selectedStepIndex === null ? 20 : 18,
|
|
|
height: selectedStepIndex === null ? 20 : 18,
|
|
|
borderRadius: '50%',
|
|
|
backgroundColor:
|
|
|
finalStep.type === 'success' ? 'success.main' :
|
|
|
finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached' ? 'warning.main' :
|
|
|
'error.main',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
transition: 'all 0.2s ease',
|
|
|
boxShadow: selectedStepIndex === null
|
|
|
? finalStep.type === 'success'
|
|
|
? '0 2px 8px rgba(102, 187, 106, 0.4)'
|
|
|
: finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached'
|
|
|
? '0 2px 8px rgba(255, 152, 0, 0.4)'
|
|
|
: '0 2px 8px rgba(244, 67, 54, 0.4)'
|
|
|
: '0 2px 4px rgba(0,0,0,0.1)',
|
|
|
position: 'relative',
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
>
|
|
|
{finalStep.type === 'success' ? (
|
|
|
<CheckIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
|
) : finalStep.type === 'stopped' ? (
|
|
|
<StopCircleIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
|
) : finalStep.type === 'max_steps_reached' ? (
|
|
|
<HourglassEmptyIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
|
) : finalStep.type === 'sandbox_timeout' ? (
|
|
|
<AccessTimeIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
|
) : (
|
|
|
<CloseIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
|
)}
|
|
|
</Box>
|
|
|
</Box>
|
|
|
|
|
|
{/* Final step label */}
|
|
|
<Typography
|
|
|
variant="caption"
|
|
|
sx={{
|
|
|
fontSize: '0.7rem',
|
|
|
fontWeight: selectedStepIndex === null ? 700 : 500,
|
|
|
color:
|
|
|
finalStep.type === 'success'
|
|
|
? (selectedStepIndex === null ? 'text.primary' : 'text.secondary')
|
|
|
: finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached'
|
|
|
? 'warning.main'
|
|
|
: 'error.main',
|
|
|
whiteSpace: 'nowrap',
|
|
|
}}
|
|
|
>
|
|
|
{finalStep.type === 'success' ? 'End' :
|
|
|
finalStep.type === 'stopped' ? 'Stopped' :
|
|
|
finalStep.type === 'max_steps_reached' ? 'Max Steps' :
|
|
|
finalStep.type === 'sandbox_timeout' ? 'Timeout' :
|
|
|
'Failed'}
|
|
|
</Typography>
|
|
|
</Box>
|
|
|
)}
|
|
|
</React.Fragment>
|
|
|
))}
|
|
|
</Box>
|
|
|
</Box>
|
|
|
</Box>
|
|
|
);
|
|
|
};
|
|
|
|