CoolFace
Apppublic

HyperCluster/Fara-BrowserUse

sourceHugging Facemitupdated 10mo agoView on Hugging Face
5likes
Header.tsx451 linesDownload Raw Back to components
1import React, { useState, useEffect, useRef } from 'react';
2import { AppBar, Toolbar, Box, Typography, Chip, IconButton, CircularProgress, keyframes, Button } from '@mui/material';
3import ArrowBackIcon from '@mui/icons-material/ArrowBack';
4import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
5import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
6import CheckIcon from '@mui/icons-material/Check';
7import CloseIcon from '@mui/icons-material/Close';
8import AccessTimeIcon from '@mui/icons-material/AccessTime';
9import InputIcon from '@mui/icons-material/Input';
10import OutputIcon from '@mui/icons-material/Output';
11import SmartToyIcon from '@mui/icons-material/SmartToy';
12import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
13import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
14import StopCircleIcon from '@mui/icons-material/StopCircle';
15import { useAgentStore, selectTrace, selectError, selectIsDarkMode, selectMetadata, selectIsConnectingToE2B, selectFinalStep } from '@/stores/agentStore';
16
17interface HeaderProps {
18  isAgentProcessing: boolean;
19  onBackToHome?: () => void;
20}
21
22// Animation for the running task border - smooth oscillation (primary)
23const borderPulse = keyframes`
24  0%, 100% {
25    border-color: rgba(79, 134, 198, 0.5);
26    box-shadow: 0 0 0 0 rgba(79, 134, 198, 0.3);
27  }
28  50% {
29    border-color: rgba(79, 134, 198, 1);
30    box-shadow: 0 0 8px 2px rgba(79, 134, 198, 0.4);
31  }
32`;
33
34// Animation for the background glow (primary)
35const backgroundPulse = keyframes`
36  0%, 100% {
37    background-color: rgba(79, 134, 198, 0.08);
38  }
39  50% {
40    background-color: rgba(79, 134, 198, 0.15);
41  }
42`;
43
44// Animation for token flash - smooth glow effect
45const tokenFlash = keyframes`
46  0% {
47    filter: brightness(1);
48    text-shadow: none;
49  }
50  25% {
51    filter: brightness(1.4);
52    text-shadow: 0 0 8px rgba(79, 134, 198, 0.6);
53  }
54  100% {
55    filter: brightness(1);
56    text-shadow: none;
57  }
58`;
59
60// Animation for token icon flash
61const iconFlash = keyframes`
62  0% {
63    filter: brightness(1);
64    transform: scale(1);
65  }
66  25% {
67    filter: brightness(1.6);
68    transform: scale(1.15);
69  }
70  100% {
71    filter: brightness(1);
72    transform: scale(1);
73  }
74`;
75
76export const Header: React.FC<HeaderProps> = ({ isAgentProcessing, onBackToHome }) => {
77  const trace = useAgentStore(selectTrace);
78  const error = useAgentStore(selectError);
79  const finalStep = useAgentStore(selectFinalStep);
80  const isDarkMode = useAgentStore(selectIsDarkMode);
81  const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode);
82  const metadata = useAgentStore(selectMetadata);
83  const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B);
84  const [elapsedTime, setElapsedTime] = useState(0);
85  const [inputTokenFlash, setInputTokenFlash] = useState(false);
86  const [outputTokenFlash, setOutputTokenFlash] = useState(false);
87  const prevInputTokens = useRef(0);
88  const prevOutputTokens = useRef(0);
89
90  // Update elapsed time every 100ms when agent is processing
91  useEffect(() => {
92    if (isAgentProcessing && trace?.timestamp) {
93      const interval = setInterval(() => {
94        const now = new Date();
95        const startTime = new Date(trace.timestamp);
96        const elapsed = (now.getTime() - startTime.getTime()) / 1000;
97        setElapsedTime(elapsed);
98      }, 100);
99
100      return () => clearInterval(interval);
101    } else if (metadata && metadata.duration > 0) {
102      setElapsedTime(metadata.duration);
103    }
104  }, [isAgentProcessing, trace?.timestamp, metadata]);
105
106  // Detect token changes and trigger flash animation
107  useEffect(() => {
108    if (metadata) {
109      // Input tokens changed
110      if (metadata.inputTokensUsed > prevInputTokens.current && prevInputTokens.current > 0) {
111        setInputTokenFlash(true);
112        setTimeout(() => setInputTokenFlash(false), 800);
113      }
114      prevInputTokens.current = metadata.inputTokensUsed;
115
116      // Output tokens changed
117      if (metadata.outputTokensUsed > prevOutputTokens.current && prevOutputTokens.current > 0) {
118        setOutputTokenFlash(true);
119        setTimeout(() => setOutputTokenFlash(false), 800);
120      }
121      prevOutputTokens.current = metadata.outputTokensUsed;
122    }
123  }, [metadata?.inputTokensUsed, metadata?.outputTokensUsed]);
124
125  // Determine task status - Use finalStep as source of truth
126  const getTaskStatus = () => {
127    // If we have a final step, use its type
128    if (finalStep) {
129      switch (finalStep.type) {
130        case 'failure':
131          return { label: 'Task failed', color: 'error', icon: <CloseIcon sx={{ fontSize: 16, color: 'error.main' }} /> };
132        case 'stopped':
133          return { label: 'Task stopped', color: 'warning', icon: <StopCircleIcon sx={{ fontSize: 16, color: 'warning.main' }} /> };
134        case 'max_steps_reached':
135          return { label: 'Max steps reached', color: 'warning', icon: <HourglassEmptyIcon sx={{ fontSize: 16, color: 'warning.main' }} /> };
136        case 'success':
137          return { label: 'Completed', color: 'success', icon: <CheckIcon sx={{ fontSize: 16, color: 'success.main' }} /> };
138      }
139    }
140    // Otherwise check running states
141    if (isConnectingToE2B) return { label: 'Starting...', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> };
142    if (isAgentProcessing || trace?.isRunning) return { label: 'Running', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> };
143    return { label: 'Ready', color: 'default', icon: <CheckIcon sx={{ fontSize: 16, color: 'text.secondary' }} /> };
144  };
145
146  const taskStatus = getTaskStatus();
147
148  // Extract model name from modelId (e.g., "Qwen/Qwen3-VL-8B-Instruct" -> "Qwen3-VL-8B-Instruct")
149  const modelName = trace?.modelId?.split('/').pop() || 'Unknown Model';
150
151  // Handler for emergency stop
152  const handleEmergencyStop = () => {
153    const stopTask = (window as Window & { __stopCurrentTask?: () => void }).__stopCurrentTask;
154    if (stopTask) {
155      stopTask();
156    }
157  };
158
159  return (
160    <AppBar
161      position="static"
162      elevation={0}
163      sx={{
164        backgroundColor: 'background.paper',
165        borderBottom: '1px solid',
166        borderColor: 'divider',
167      }}
168    >
169      <Toolbar disableGutters sx={{ px: 2, py: 2.5, flexDirection: 'column', alignItems: 'stretch', gap: 0 }}>
170        {/* First row: Back button + Task info + Connection Status */}
171        <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', gap: 3 }}>
172          {/* Left side: Back button + Task info */}
173          <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 0 }}>
174            <IconButton
175              onClick={onBackToHome}
176              size="small"
177              sx={{
178                color: 'primary.main',
179                backgroundColor: 'primary.50',
180                border: '1px solid',
181                borderColor: 'primary.200',
182                cursor: 'pointer',
183                '&:hover': {
184                  backgroundColor: 'primary.100',
185                  borderColor: 'primary.main',
186                },
187              }}
188            >
189              <ArrowBackIcon fontSize="small" />
190            </IconButton>
191            <Typography
192              variant="body2"
193              sx={{
194                color: 'text.primary',
195                fontWeight: 700,
196                fontSize: '1rem',
197                overflow: 'hidden',
198                textOverflow: 'ellipsis',
199                whiteSpace: 'nowrap',
200              }}
201            >
202              {trace?.instruction || 'No task running'}
203            </Typography>
204          </Box>
205
206          {/* Right side: Emergency Stop + Dark Mode */}
207          <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
208            {/* Emergency Stop Button - Only show when agent is processing */}
209            {isAgentProcessing && (
210              <Button
211                onClick={handleEmergencyStop}
212                variant="outlined"
213                size="small"
214                startIcon={<StopCircleIcon />}
215                sx={{
216                  color: 'error.main',
217                  borderColor: 'error.main',
218                  backgroundColor: 'transparent',
219                  fontWeight: 600,
220                  fontSize: '0.8rem',
221                  px: 1.5,
222                  py: 0.5,
223                  textTransform: 'none',
224                  '&:hover': {
225                    backgroundColor: 'error.50',
226                    borderColor: 'error.dark',
227                  },
228                }}
229              >
230                Stop
231              </Button>
232            )}
233
234            <IconButton
235              onClick={toggleDarkMode}
236              size="small"
237              sx={{
238                color: 'primary.main',
239                backgroundColor: 'primary.50',
240                border: '1px solid',
241                borderColor: 'primary.200',
242                '&:hover': {
243                  backgroundColor: 'primary.100',
244                  borderColor: 'primary.main',
245                },
246              }}
247            >
248              {isDarkMode ? <LightModeOutlined fontSize="small" /> : <DarkModeOutlined fontSize="small" />}
249            </IconButton>
250          </Box>
251        </Box>
252
253        {/* Second row: Status + Model + Metadata - Only show when we have trace data */}
254        {trace && (
255          <Box
256            sx={{
257              display: 'flex',
258              alignItems: 'center',
259              gap: 1.5,
260              pl: 5.5,
261              pr: 1,
262              pt: .5,
263              mt: .5,
264            }}
265          >
266            {/* Status Badge - Compact */}
267            <Box
268              sx={{
269                display: 'flex',
270                alignItems: 'center',
271                gap: 0.5,
272                px: 1,
273                py: 0.25,
274                borderRadius: 1,
275                backgroundColor:
276                  taskStatus.color === 'primary' ? 'primary.50' :
277                  taskStatus.color === 'success' ? 'success.50' :
278                  taskStatus.color === 'error' ? 'error.50' :
279                  taskStatus.color === 'warning' ? 'warning.50' :
280                  'action.hover',
281                border: '1px solid',
282                borderColor:
283                  taskStatus.color === 'primary' ? 'primary.main' :
284                  taskStatus.color === 'success' ? 'success.main' :
285                  taskStatus.color === 'error' ? 'error.main' :
286                  taskStatus.color === 'warning' ? 'warning.main' :
287                  'divider',
288              }}
289            >
290              {taskStatus.icon}
291              <Typography
292                variant="caption"
293                sx={{
294                  fontSize: '0.7rem',
295                  fontWeight: 700,
296                  color:
297                    taskStatus.color === 'primary' ? 'primary.main' :
298                    taskStatus.color === 'success' ? 'success.main' :
299                    taskStatus.color === 'error' ? 'error.main' :
300                    taskStatus.color === 'warning' ? 'warning.main' :
301                    'text.primary',
302                }}
303              >
304                {taskStatus.label}
305              </Typography>
306            </Box>
307
308            {/* Divider */}
309            <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
310
311            {/* Model */}
312            <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
313              <SmartToyIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
314              <Typography
315                variant="caption"
316                sx={{
317                  fontSize: '0.75rem',
318                  fontWeight: 600,
319                  color: 'text.primary',
320                }}
321              >
322                {modelName}
323              </Typography>
324            </Box>
325
326            {/* Steps Count */}
327            {metadata && (
328              <>
329                <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
330                <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
331                  <Typography
332                    variant="caption"
333                    sx={{
334                      fontSize: '0.75rem',
335                      fontWeight: 700,
336                      color: 'text.primary',
337                      mr: 0.5,
338                    }}
339                  >
340                    {metadata.numberOfSteps}
341                  </Typography>
342                  <Typography
343                    variant="caption"
344                    sx={{
345                      fontSize: '0.7rem',
346                      fontWeight: 400,
347                      color: 'text.secondary',
348                    }}
349                  >
350                    {metadata.numberOfSteps === 1 ? 'Step' : 'Steps'}
351                  </Typography>
352                </Box>
353              </>
354            )}
355
356            {/* Time */}
357            {(isAgentProcessing || metadata) && (
358              <>
359                <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
360                <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
361                  <AccessTimeIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
362                  <Typography
363                    variant="caption"
364                    sx={{
365                      fontSize: '0.75rem',
366                      fontWeight: 700,
367                      color: 'text.primary',
368                      minWidth: '45px',
369                      textAlign: 'left',
370                    }}
371                  >
372                    {elapsedTime.toFixed(1)}s
373                  </Typography>
374                </Box>
375              </>
376            )}
377
378            {/* Input Tokens */}
379            {metadata && metadata.inputTokensUsed > 0 && (
380              <>
381                <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
382                <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
383                  <InputIcon
384                    sx={{
385                      fontSize: '0.85rem',
386                      color: 'primary.main',
387                      transition: 'all 0.2s ease',
388                      animation: inputTokenFlash ? `${iconFlash} 0.8s ease-out` : 'none',
389                    }}
390                  />
391                  <Box
392                    sx={{
393                      transition: 'all 0.2s ease',
394                      animation: inputTokenFlash ? `${tokenFlash} 0.8s ease-out` : 'none',
395                    }}
396                  >
397                    <Typography
398                      variant="caption"
399                      sx={{
400                        fontSize: '0.75rem',
401                        fontWeight: 700,
402                        color: 'text.primary',
403                      }}
404                    >
405                      {metadata.inputTokensUsed.toLocaleString()}
406                    </Typography>
407                  </Box>
408                </Box>
409              </>
410            )}
411
412            {/* Output Tokens */}
413            {metadata && metadata.outputTokensUsed > 0 && (
414              <>
415                <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
416                <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
417                  <OutputIcon
418                    sx={{
419                      fontSize: '0.85rem',
420                      color: 'primary.main',
421                      transition: 'all 0.2s ease',
422                      animation: outputTokenFlash ? `${iconFlash} 0.8s ease-out` : 'none',
423                    }}
424                  />
425                  <Box
426                    sx={{
427                      transition: 'all 0.2s ease',
428                      animation: outputTokenFlash ? `${tokenFlash} 0.8s ease-out` : 'none',
429                    }}
430                  >
431                    <Typography
432                      variant="caption"
433                      sx={{
434                        fontSize: '0.75rem',
435                        fontWeight: 700,
436                        color: 'text.primary',
437                      }}
438                    >
439                      {metadata.outputTokensUsed.toLocaleString()}
440                    </Typography>
441                  </Box>
442                </Box>
443              </>
444            )}
445          </Box>
446        )}
447      </Toolbar>
448    </AppBar>
449  );
450};
451