CoolFace
Apppublic

HyperCluster/Fara-BrowserUse

sourceHugging Facemitupdated 10mo agoView on Hugging Face
5likes
Timeline.tsx487 linesDownload Raw Back to timeline
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