|
|
import React, { useState, useEffect, useRef } from 'react'; |
|
|
import { AppBar, Toolbar, Box, Typography, Chip, IconButton, CircularProgress, keyframes, Button } from '@mui/material'; |
|
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; |
|
|
import LightModeOutlined from '@mui/icons-material/LightModeOutlined'; |
|
|
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; |
|
|
import CheckIcon from '@mui/icons-material/Check'; |
|
|
import CloseIcon from '@mui/icons-material/Close'; |
|
|
import AccessTimeIcon from '@mui/icons-material/AccessTime'; |
|
|
import InputIcon from '@mui/icons-material/Input'; |
|
|
import OutputIcon from '@mui/icons-material/Output'; |
|
|
import SmartToyIcon from '@mui/icons-material/SmartToy'; |
|
|
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
|
|
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; |
|
|
import StopCircleIcon from '@mui/icons-material/StopCircle'; |
|
|
import { useAgentStore, selectTrace, selectError, selectIsDarkMode, selectMetadata, selectIsConnectingToE2B, selectFinalStep } from '@/stores/agentStore'; |
|
|
|
|
|
interface HeaderProps { |
|
|
isAgentProcessing: boolean; |
|
|
onBackToHome?: () => void; |
|
|
} |
|
|
|
|
|
|
|
|
const borderPulse = keyframes` |
|
|
0%, 100% { |
|
|
border-color: rgba(79, 134, 198, 0.5); |
|
|
box-shadow: 0 0 0 0 rgba(79, 134, 198, 0.3); |
|
|
} |
|
|
50% { |
|
|
border-color: rgba(79, 134, 198, 1); |
|
|
box-shadow: 0 0 8px 2px rgba(79, 134, 198, 0.4); |
|
|
} |
|
|
`; |
|
|
|
|
|
|
|
|
const backgroundPulse = keyframes` |
|
|
0%, 100% { |
|
|
background-color: rgba(79, 134, 198, 0.08); |
|
|
} |
|
|
50% { |
|
|
background-color: rgba(79, 134, 198, 0.15); |
|
|
} |
|
|
`; |
|
|
|
|
|
|
|
|
const tokenFlash = keyframes` |
|
|
0% { |
|
|
filter: brightness(1); |
|
|
text-shadow: none; |
|
|
} |
|
|
25% { |
|
|
filter: brightness(1.4); |
|
|
text-shadow: 0 0 8px rgba(79, 134, 198, 0.6); |
|
|
} |
|
|
100% { |
|
|
filter: brightness(1); |
|
|
text-shadow: none; |
|
|
} |
|
|
`; |
|
|
|
|
|
|
|
|
const iconFlash = keyframes` |
|
|
0% { |
|
|
filter: brightness(1); |
|
|
transform: scale(1); |
|
|
} |
|
|
25% { |
|
|
filter: brightness(1.6); |
|
|
transform: scale(1.15); |
|
|
} |
|
|
100% { |
|
|
filter: brightness(1); |
|
|
transform: scale(1); |
|
|
} |
|
|
`; |
|
|
|
|
|
export const Header: React.FC<HeaderProps> = ({ isAgentProcessing, onBackToHome }) => { |
|
|
const trace = useAgentStore(selectTrace); |
|
|
const error = useAgentStore(selectError); |
|
|
const finalStep = useAgentStore(selectFinalStep); |
|
|
const isDarkMode = useAgentStore(selectIsDarkMode); |
|
|
const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode); |
|
|
const metadata = useAgentStore(selectMetadata); |
|
|
const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B); |
|
|
const [elapsedTime, setElapsedTime] = useState(0); |
|
|
const [inputTokenFlash, setInputTokenFlash] = useState(false); |
|
|
const [outputTokenFlash, setOutputTokenFlash] = useState(false); |
|
|
const prevInputTokens = useRef(0); |
|
|
const prevOutputTokens = useRef(0); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (isAgentProcessing && trace?.timestamp) { |
|
|
const interval = setInterval(() => { |
|
|
const now = new Date(); |
|
|
const startTime = new Date(trace.timestamp); |
|
|
const elapsed = (now.getTime() - startTime.getTime()) / 1000; |
|
|
setElapsedTime(elapsed); |
|
|
}, 100); |
|
|
|
|
|
return () => clearInterval(interval); |
|
|
} else if (metadata && metadata.duration > 0) { |
|
|
setElapsedTime(metadata.duration); |
|
|
} |
|
|
}, [isAgentProcessing, trace?.timestamp, metadata]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (metadata) { |
|
|
|
|
|
if (metadata.inputTokensUsed > prevInputTokens.current && prevInputTokens.current > 0) { |
|
|
setInputTokenFlash(true); |
|
|
setTimeout(() => setInputTokenFlash(false), 800); |
|
|
} |
|
|
prevInputTokens.current = metadata.inputTokensUsed; |
|
|
|
|
|
|
|
|
if (metadata.outputTokensUsed > prevOutputTokens.current && prevOutputTokens.current > 0) { |
|
|
setOutputTokenFlash(true); |
|
|
setTimeout(() => setOutputTokenFlash(false), 800); |
|
|
} |
|
|
prevOutputTokens.current = metadata.outputTokensUsed; |
|
|
} |
|
|
}, [metadata?.inputTokensUsed, metadata?.outputTokensUsed]); |
|
|
|
|
|
|
|
|
const getTaskStatus = () => { |
|
|
|
|
|
if (finalStep) { |
|
|
switch (finalStep.type) { |
|
|
case 'failure': |
|
|
return { label: 'Task failed', color: 'error', icon: <CloseIcon sx={{ fontSize: 16, color: 'error.main' }} /> }; |
|
|
case 'stopped': |
|
|
return { label: 'Task stopped', color: 'warning', icon: <StopCircleIcon sx={{ fontSize: 16, color: 'warning.main' }} /> }; |
|
|
case 'max_steps_reached': |
|
|
return { label: 'Max steps reached', color: 'warning', icon: <HourglassEmptyIcon sx={{ fontSize: 16, color: 'warning.main' }} /> }; |
|
|
case 'success': |
|
|
return { label: 'Completed', color: 'success', icon: <CheckIcon sx={{ fontSize: 16, color: 'success.main' }} /> }; |
|
|
} |
|
|
} |
|
|
|
|
|
if (isConnectingToE2B) return { label: 'Connecting to E2B...', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> }; |
|
|
if (isAgentProcessing || trace?.isRunning) return { label: 'Running', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> }; |
|
|
return { label: 'Ready', color: 'default', icon: <CheckIcon sx={{ fontSize: 16, color: 'text.secondary' }} /> }; |
|
|
}; |
|
|
|
|
|
const taskStatus = getTaskStatus(); |
|
|
|
|
|
|
|
|
const modelName = trace?.modelId?.split('/').pop() || 'Unknown Model'; |
|
|
|
|
|
|
|
|
const handleEmergencyStop = () => { |
|
|
const stopTask = (window as Window & { __stopCurrentTask?: () => void }).__stopCurrentTask; |
|
|
if (stopTask) { |
|
|
stopTask(); |
|
|
} |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<AppBar |
|
|
position="static" |
|
|
elevation={0} |
|
|
sx={{ |
|
|
backgroundColor: 'background.paper', |
|
|
borderBottom: '1px solid', |
|
|
borderColor: 'divider', |
|
|
}} |
|
|
> |
|
|
<Toolbar disableGutters sx={{ px: 2, py: 2.5, flexDirection: 'column', alignItems: 'stretch', gap: 0 }}> |
|
|
{/* First row: Back button + Task info + Connection Status */} |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', gap: 3 }}> |
|
|
{/* Left side: Back button + Task info */} |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 0 }}> |
|
|
<IconButton |
|
|
onClick={onBackToHome} |
|
|
size="small" |
|
|
sx={{ |
|
|
color: 'primary.main', |
|
|
backgroundColor: 'primary.50', |
|
|
border: '1px solid', |
|
|
borderColor: 'primary.200', |
|
|
cursor: 'pointer', |
|
|
'&:hover': { |
|
|
backgroundColor: 'primary.100', |
|
|
borderColor: 'primary.main', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<ArrowBackIcon fontSize="small" /> |
|
|
</IconButton> |
|
|
<Typography |
|
|
variant="body2" |
|
|
sx={{ |
|
|
color: 'text.primary', |
|
|
fontWeight: 700, |
|
|
fontSize: '1rem', |
|
|
overflow: 'hidden', |
|
|
textOverflow: 'ellipsis', |
|
|
whiteSpace: 'nowrap', |
|
|
}} |
|
|
> |
|
|
{trace?.instruction || 'No task running'} |
|
|
</Typography> |
|
|
</Box> |
|
|
|
|
|
{/* Right side: Emergency Stop + Dark Mode */} |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
|
|
{/* Emergency Stop Button - Only show when agent is processing */} |
|
|
{isAgentProcessing && ( |
|
|
<Button |
|
|
onClick={handleEmergencyStop} |
|
|
variant="outlined" |
|
|
size="small" |
|
|
startIcon={<StopCircleIcon />} |
|
|
sx={{ |
|
|
color: 'error.main', |
|
|
borderColor: 'error.main', |
|
|
backgroundColor: 'transparent', |
|
|
fontWeight: 600, |
|
|
fontSize: '0.8rem', |
|
|
px: 1.5, |
|
|
py: 0.5, |
|
|
textTransform: 'none', |
|
|
'&:hover': { |
|
|
backgroundColor: 'error.50', |
|
|
borderColor: 'error.dark', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
Stop |
|
|
</Button> |
|
|
)} |
|
|
|
|
|
<IconButton |
|
|
onClick={toggleDarkMode} |
|
|
size="small" |
|
|
sx={{ |
|
|
color: 'primary.main', |
|
|
backgroundColor: 'primary.50', |
|
|
border: '1px solid', |
|
|
borderColor: 'primary.200', |
|
|
'&:hover': { |
|
|
backgroundColor: 'primary.100', |
|
|
borderColor: 'primary.main', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
{isDarkMode ? <LightModeOutlined fontSize="small" /> : <DarkModeOutlined fontSize="small" />} |
|
|
</IconButton> |
|
|
</Box> |
|
|
</Box> |
|
|
|
|
|
{/* Second row: Status + Model + Metadata - Only show when we have trace data */} |
|
|
{trace && ( |
|
|
<Box |
|
|
sx={{ |
|
|
display: 'flex', |
|
|
alignItems: 'center', |
|
|
gap: 1.5, |
|
|
pl: 5.5, |
|
|
pr: 1, |
|
|
pt: .5, |
|
|
mt: .5, |
|
|
}} |
|
|
> |
|
|
{/* Status Badge - Compact */} |
|
|
<Box |
|
|
sx={{ |
|
|
display: 'flex', |
|
|
alignItems: 'center', |
|
|
gap: 0.5, |
|
|
px: 1, |
|
|
py: 0.25, |
|
|
borderRadius: 1, |
|
|
backgroundColor: |
|
|
taskStatus.color === 'primary' ? 'primary.50' : |
|
|
taskStatus.color === 'success' ? 'success.50' : |
|
|
taskStatus.color === 'error' ? 'error.50' : |
|
|
taskStatus.color === 'warning' ? 'warning.50' : |
|
|
'action.hover', |
|
|
border: '1px solid', |
|
|
borderColor: |
|
|
taskStatus.color === 'primary' ? 'primary.main' : |
|
|
taskStatus.color === 'success' ? 'success.main' : |
|
|
taskStatus.color === 'error' ? 'error.main' : |
|
|
taskStatus.color === 'warning' ? 'warning.main' : |
|
|
'divider', |
|
|
}} |
|
|
> |
|
|
{taskStatus.icon} |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.7rem', |
|
|
fontWeight: 700, |
|
|
color: |
|
|
taskStatus.color === 'primary' ? 'primary.main' : |
|
|
taskStatus.color === 'success' ? 'success.main' : |
|
|
taskStatus.color === 'error' ? 'error.main' : |
|
|
taskStatus.color === 'warning' ? 'warning.main' : |
|
|
'text.primary', |
|
|
}} |
|
|
> |
|
|
{taskStatus.label} |
|
|
</Typography> |
|
|
</Box> |
|
|
|
|
|
{/* Divider */} |
|
|
<Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} /> |
|
|
|
|
|
{/* Model */} |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<SmartToyIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} /> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
fontWeight: 600, |
|
|
color: 'text.primary', |
|
|
}} |
|
|
> |
|
|
{modelName} |
|
|
</Typography> |
|
|
</Box> |
|
|
|
|
|
{/* Steps Count */} |
|
|
{metadata && ( |
|
|
<> |
|
|
<Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} /> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
fontWeight: 700, |
|
|
color: 'text.primary', |
|
|
mr: 0.5, |
|
|
}} |
|
|
> |
|
|
{metadata.numberOfSteps} |
|
|
</Typography> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.7rem', |
|
|
fontWeight: 400, |
|
|
color: 'text.secondary', |
|
|
}} |
|
|
> |
|
|
{metadata.numberOfSteps === 1 ? 'Step' : 'Steps'} |
|
|
</Typography> |
|
|
</Box> |
|
|
</> |
|
|
)} |
|
|
|
|
|
{/* Time */} |
|
|
{(isAgentProcessing || metadata) && ( |
|
|
<> |
|
|
<Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} /> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<AccessTimeIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} /> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
fontWeight: 700, |
|
|
color: 'text.primary', |
|
|
minWidth: '45px', |
|
|
textAlign: 'left', |
|
|
}} |
|
|
> |
|
|
{elapsedTime.toFixed(1)}s |
|
|
</Typography> |
|
|
</Box> |
|
|
</> |
|
|
)} |
|
|
|
|
|
{/* Input Tokens */} |
|
|
{metadata && metadata.inputTokensUsed > 0 && ( |
|
|
<> |
|
|
<Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} /> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<InputIcon |
|
|
sx={{ |
|
|
fontSize: '0.85rem', |
|
|
color: 'primary.main', |
|
|
transition: 'all 0.2s ease', |
|
|
animation: inputTokenFlash ? `${iconFlash} 0.8s ease-out` : 'none', |
|
|
}} |
|
|
/> |
|
|
<Box |
|
|
sx={{ |
|
|
transition: 'all 0.2s ease', |
|
|
animation: inputTokenFlash ? `${tokenFlash} 0.8s ease-out` : 'none', |
|
|
}} |
|
|
> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
fontWeight: 700, |
|
|
color: 'text.primary', |
|
|
}} |
|
|
> |
|
|
{metadata.inputTokensUsed.toLocaleString()} |
|
|
</Typography> |
|
|
</Box> |
|
|
</Box> |
|
|
</> |
|
|
)} |
|
|
|
|
|
{} |
|
|
{metadata && metadata.outputTokensUsed > 0 && ( |
|
|
<> |
|
|
<Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} /> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<OutputIcon |
|
|
sx={{ |
|
|
fontSize: '0.85rem', |
|
|
color: 'primary.main', |
|
|
transition: 'all 0.2s ease', |
|
|
animation: outputTokenFlash ? `${iconFlash} 0.8s ease-out` : 'none', |
|
|
}} |
|
|
/> |
|
|
<Box |
|
|
sx={{ |
|
|
transition: 'all 0.2s ease', |
|
|
animation: outputTokenFlash ? `${tokenFlash} 0.8s ease-out` : 'none', |
|
|
}} |
|
|
> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
fontWeight: 700, |
|
|
color: 'text.primary', |
|
|
}} |
|
|
> |
|
|
{metadata.outputTokensUsed.toLocaleString()} |
|
|
</Typography> |
|
|
</Box> |
|
|
</Box> |
|
|
</> |
|
|
)} |
|
|
</Box> |
|
|
)} |
|
|
</Toolbar> |
|
|
</AppBar> |
|
|
); |
|
|
}; |
|
|
|