HyperCluster/Fara-BrowserUse
5
1import React, { useRef, useEffect } from 'react';
2import { Box, Typography, CircularProgress, Button } from '@mui/material';
3import CheckIcon from '@mui/icons-material/Check';
4import CloseIcon from '@mui/icons-material/Close';
5import StopCircleIcon from '@mui/icons-material/StopCircle';
6import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
7import AccessTimeIcon from '@mui/icons-material/AccessTime';
8import CableIcon from '@mui/icons-material/Cable';
9import { AgentTraceMetadata } from '@/types/agent';
10import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore';
11
12interface TimelineProps {
13 metadata: AgentTraceMetadata;
14 isRunning: boolean;
15}
16
17export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
18 const timelineRef = useRef<HTMLDivElement>(null);
19 const selectedStepIndex = useAgentStore(selectSelectedStepIndex);
20 const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
21 const finalStep = useAgentStore(selectFinalStep);
22 const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B);
23 const isAgentProcessing = useAgentStore(selectIsAgentProcessing);
24
25 // Show connection indicator if connecting or if we have started processing
26 const showConnectionIndicator = isConnectingToE2B || isAgentProcessing || (metadata.numberOfSteps > 0) || finalStep;
27
28 // Generate array of steps with their status
29 // Only show completed steps + current step if running
30 const totalStepsToShow = isRunning && !isConnectingToE2B
31 ? metadata.numberOfSteps + 1 // Show completed steps + current step
32 : metadata.numberOfSteps; // Show only completed steps when not running
33
34 // Calculate total width for the line (including finalStep if present)
35 const lineWidth = finalStep
36 ? `calc(${totalStepsToShow} * (40px + 12px) + 52px)` // Add space for finalStep (40px + 12px gap)
37 : `calc(${totalStepsToShow} * (40px + 12px))`;
38
39 const steps = Array.from({ length: totalStepsToShow }, (_, index) => ({
40 stepNumber: index + 1,
41 stepIndex: index,
42 isCompleted: index < metadata.numberOfSteps,
43 // Step is current if: we're at the right index AND running AND not connecting to E2B
44 isCurrent: (index === metadata.numberOfSteps && isRunning && !isConnectingToE2B) ||
45 (index === 0 && metadata.numberOfSteps === 0 && isRunning && !isConnectingToE2B),
46 isSelected: selectedStepIndex === index,
47 }));
48
49 // Handle step click
50 const handleStepClick = (stepIndex: number, isCompleted: boolean, isCurrent: boolean) => {
51 if (isCompleted) {
52 setSelectedStepIndex(stepIndex);
53 } else if (isCurrent) {
54 // Clicking on the current step (with animation) goes back to live mode
55 setSelectedStepIndex(null);
56 }
57 };
58
59 // Handle final step click (goes to live mode showing the final status)
60 const handleFinalStepClick = () => {
61 setSelectedStepIndex(null);
62 };
63
64 // Auto-scroll to current step while running
65 useEffect(() => {
66 if (timelineRef.current && isRunning) {
67 // Only auto-scroll while running, not when finished
68 const currentStepElement = timelineRef.current.querySelector(`[data-step="${metadata.numberOfSteps}"]`);
69 if (currentStepElement) {
70 currentStepElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
71 }
72 }
73 }, [metadata.numberOfSteps, isRunning]);
74
75 return (
76 <Box
77 sx={{
78 p: 2,
79 border: '1px solid',
80 borderColor: 'divider',
81 borderRadius: '12px',
82 backgroundColor: 'background.paper',
83 flexShrink: 0,
84 }}
85 >
86 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
87 {/* Header with step count */}
88 <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
89 <Typography variant="h6" sx={{ fontSize: '0.9rem', fontWeight: 700, color: 'text.primary' }}>
90 Timeline
91 {selectedStepIndex !== null && (
92 <Typography component="span" sx={{ ml: 1, color: 'text.secondary', fontWeight: 500, fontSize: '0.65rem' }}>
93 - Viewing step {selectedStepIndex + 1}
94 </Typography>
95 )}
96 </Typography>
97 {selectedStepIndex !== null && (
98 <Button
99 size="small"
100 variant="outlined"
101 onClick={handleFinalStepClick}
102 sx={{
103 textTransform: 'none',
104 fontSize: '0.7rem',
105 fontWeight: 600,
106 px: 1.5,
107 py: 0.25,
108 minWidth: 'auto',
109 color: 'text.secondary',
110 borderColor: 'divider',
111 '&:hover': {
112 backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
113 borderColor: 'text.secondary',
114 },
115 }}
116 >
117 Back to latest step
118 </Button>
119 )}
120 </Box>
121
122 {/* Horizontal scrollable step indicators */}
123 <Box
124 ref={timelineRef}
125 sx={{
126 display: 'flex',
127 alignItems: 'center',
128 overflowX: 'auto',
129 overflowY: 'hidden',
130 gap: 1.5,
131 py: 1.5,
132 height: 60,
133 position: 'relative',
134 // Hide scrollbar completely
135 scrollbarWidth: 'none', // Firefox
136 '&::-webkit-scrollbar': {
137 display: 'none', // Chrome, Safari, Edge
138 },
139 // Horizontal line crossing through circles
140 '&::before': {
141 content: '""',
142 position: 'absolute',
143 left: "25px",
144 // Calculate width to cover visible steps + finalStep if present
145 width: lineWidth,
146 top: '19.5px',
147 transform: 'translateY(-50%)',
148 transition: 'width 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
149 height: '2px',
150 backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.3)',
151 zIndex: 0,
152 pointerEvents: 'none',
153 },
154 }}
155 >
156 {/* Connection indicator (step 0) */}
157 {showConnectionIndicator && (
158 <Box
159 data-step="connection"
160 sx={{
161 display: 'flex',
162 flexDirection: 'column',
163 alignItems: 'center',
164 gap: 0.75,
165 minWidth: 40,
166 flexShrink: 0,
167 position: 'relative',
168 zIndex: 1,
169 }}
170 >
171 {/* White circle background to hide the line */}
172 <Box
173 sx={{
174 position: 'relative',
175 display: 'flex',
176 alignItems: 'center',
177 justifyContent: 'center',
178 height: 28,
179 width: 28,
180 }}
181 >
182 {/* White background to hide the line */}
183 <Box
184 sx={{
185 position: 'absolute',
186 width: 28,
187 height: 28,
188 borderRadius: '50%',
189 backgroundColor: 'background.paper',
190 zIndex: 0,
191 }}
192 />
193
194 {/* Connection icon */}
195 {isConnectingToE2B ? (
196 <CircularProgress
197 size={20}
198 thickness={5}
199 sx={{
200 color: 'primary.main',
201 position: 'relative',
202 zIndex: 1,
203 }}
204 />
205 ) : (
206 <CableIcon
207 sx={{
208 fontSize: 20,
209 color: 'success.main',
210 position: 'relative',
211 zIndex: 1,
212 }}
213 />
214 )}
215 </Box>
216
217 {/* Connection label */}
218 <Typography
219 variant="caption"
220 sx={{
221 fontSize: '0.7rem',
222 fontWeight: 700,
223 color: isConnectingToE2B ? 'primary.main' : 'success.main',
224 whiteSpace: 'nowrap',
225 }}
226 >
227 {isConnectingToE2B ? 'Connecting' : 'Connected'}
228 </Typography>
229 </Box>
230 )}
231
232 {/* Render steps and insert final step at the right position */}
233 {steps.map((step, index) => (
234 <React.Fragment key={step.stepNumber}>
235 <Box
236 data-step={step.stepNumber}
237 onClick={() => handleStepClick(step.stepIndex, step.isCompleted, step.isCurrent)}
238 sx={{
239 display: 'flex',
240 flexDirection: 'column',
241 alignItems: 'center',
242 gap: 0.75,
243 minWidth: 40,
244 flexShrink: 0,
245 position: 'relative',
246 zIndex: 1,
247 cursor: (step.isCompleted || step.isCurrent) ? 'pointer' : 'default',
248 '&:hover': (step.isCompleted || step.isCurrent) ? {
249 '& .step-dot': {
250 transform: 'scale(1.15)',
251 },
252 } : {},
253 }}
254 >
255 {/* White circle background to hide the line */}
256 <Box
257 sx={{
258 position: 'relative',
259 display: 'flex',
260 alignItems: 'center',
261 justifyContent: 'center',
262 height: 28,
263 width: 28,
264 }}
265 >
266 {/* White background to hide the line */}
267 <Box
268 sx={{
269 position: 'absolute',
270 width: 28,
271 height: 28,
272 borderRadius: '50%',
273 backgroundColor: 'background.paper',
274 zIndex: 0,
275 }}
276 />
277
278 {/* Step dot */}
279 {step.isCurrent ? (
280 <Box
281 sx={{
282 position: 'relative',
283 display: 'flex',
284 alignItems: 'center',
285 justifyContent: 'center',
286 zIndex: 1,
287 }}
288 >
289 <CircularProgress
290 size={20}
291 thickness={5}
292 sx={{
293 color: 'primary.main',
294 position: 'absolute',
295 }}
296 />
297 <Box
298 sx={{
299 width: 8,
300 height: 8,
301 borderRadius: '50%',
302 backgroundColor: 'white',
303 position: 'absolute',
304 pointerEvents: 'none',
305 boxShadow: '0 0 4px rgba(0,0,0,0.2)',
306 }}
307 />
308 </Box>
309 ) : (
310 <Box
311 sx={{
312 position: 'relative',
313 display: 'flex',
314 alignItems: 'center',
315 justifyContent: 'center',
316 zIndex: 1,
317 }}
318 >
319 <Box
320 className="step-dot"
321 sx={{
322 width: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
323 height: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
324 borderRadius: '50%',
325 // Always keep steps in primary color (blue)
326 backgroundColor: step.isCompleted
327 ? 'primary.main' // Blue for completed steps
328 : (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.300', // Light grey for future steps
329 transition: 'all 0.2s ease',
330 boxShadow: step.isCompleted || step.isSelected
331 ? step.isSelected
332 ? '0 0 8px rgba(255, 167, 38, 0.5)'
333 : '0 2px 4px rgba(0,0,0,0.1)'
334 : 'none',
335 }}
336 />
337 {/* White dot for selected step */}
338 {step.isSelected && (
339 <Box
340 sx={{
341 width: 8,
342 height: 8,
343 borderRadius: '50%',
344 backgroundColor: 'white',
345 position: 'absolute',
346 }}
347 />
348 )}
349 </Box>
350 )}
351 </Box>
352
353 {/* Step number - show for all steps */}
354 <Typography
355 variant="caption"
356 sx={{
357 fontSize: '0.7rem',
358 fontWeight: step.isSelected || step.isCurrent ? 900 : 400,
359 color: step.isCurrent
360 ? 'primary.main'
361 : (step.isCompleted || step.isSelected
362 ? 'text.primary'
363 : (theme) => theme.palette.mode === 'dark' ? 'grey.700' : 'grey.400'),
364 whiteSpace: 'nowrap',
365 lineHeight: 1,
366 }}
367 >
368 {step.stepNumber}
369 </Typography>
370 </Box>
371
372 {/* Insert final step indicator right after the last completed step */}
373 {finalStep && step.stepNumber === metadata.numberOfSteps && (
374 <Box
375 data-step="final"
376 onClick={handleFinalStepClick}
377 sx={{
378 display: 'flex',
379 flexDirection: 'column',
380 alignItems: 'center',
381 gap: 0.75,
382 minWidth: 40,
383 flexShrink: 0,
384 position: 'relative',
385 zIndex: 1,
386 cursor: 'pointer',
387 '&:hover': {
388 '& .final-step-icon': {
389 transform: 'scale(1.15)',
390 },
391 },
392 }}
393 >
394 {/* White circle background to hide the line */}
395 <Box
396 sx={{
397 position: 'relative',
398 display: 'flex',
399 alignItems: 'center',
400 justifyContent: 'center',
401 height: 28,
402 width: 28,
403 }}
404 >
405 {/* White background to hide the line */}
406 <Box
407 sx={{
408 position: 'absolute',
409 width: 28,
410 height: 28,
411 borderRadius: '50%',
412 backgroundColor: 'background.paper',
413 zIndex: 0,
414 }}
415 />
416
417 {/* Final step icon */}
418 <Box
419 className="final-step-icon"
420 sx={{
421 width: selectedStepIndex === null ? 20 : 18,
422 height: selectedStepIndex === null ? 20 : 18,
423 borderRadius: '50%',
424 backgroundColor:
425 finalStep.type === 'success' ? 'success.main' :
426 finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached' ? 'warning.main' :
427 'error.main',
428 display: 'flex',
429 alignItems: 'center',
430 justifyContent: 'center',
431 transition: 'all 0.2s ease',
432 boxShadow: selectedStepIndex === null
433 ? finalStep.type === 'success'
434 ? '0 2px 8px rgba(102, 187, 106, 0.4)'
435 : finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached'
436 ? '0 2px 8px rgba(255, 152, 0, 0.4)'
437 : '0 2px 8px rgba(244, 67, 54, 0.4)'
438 : '0 2px 4px rgba(0,0,0,0.1)',
439 position: 'relative',
440 zIndex: 1,
441 }}
442 >
443 {finalStep.type === 'success' ? (
444 <CheckIcon sx={{ fontSize: 14, color: 'white' }} />
445 ) : finalStep.type === 'stopped' ? (
446 <StopCircleIcon sx={{ fontSize: 14, color: 'white' }} />
447 ) : finalStep.type === 'max_steps_reached' ? (
448 <HourglassEmptyIcon sx={{ fontSize: 14, color: 'white' }} />
449 ) : finalStep.type === 'sandbox_timeout' ? (
450 <AccessTimeIcon sx={{ fontSize: 14, color: 'white' }} />
451 ) : (
452 <CloseIcon sx={{ fontSize: 14, color: 'white' }} />
453 )}
454 </Box>
455 </Box>
456
457 {/* Final step label */}
458 <Typography
459 variant="caption"
460 sx={{
461 fontSize: '0.7rem',
462 fontWeight: selectedStepIndex === null ? 700 : 500,
463 color:
464 finalStep.type === 'success'
465 ? (selectedStepIndex === null ? 'text.primary' : 'text.secondary')
466 : finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached'
467 ? 'warning.main'
468 : 'error.main',
469 whiteSpace: 'nowrap',
470 }}
471 >
472 {finalStep.type === 'success' ? 'End' :
473 finalStep.type === 'stopped' ? 'Stopped' :
474 finalStep.type === 'max_steps_reached' ? 'Max Steps' :
475 finalStep.type === 'sandbox_timeout' ? 'Timeout' :
476 'Failed'}
477 </Typography>
478 </Box>
479 )}
480 </React.Fragment>
481 ))}
482 </Box>
483 </Box>
484 </Box>
485 );
486};
487 