|
|
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: 'Starting...', 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>
|
|
|
);
|
|
|
};
|
|
|
|