HyperCluster/Fara-BrowserUse
5
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 