Standardize agent event payloads and team activity timeline UI

This commit is contained in:
ي
2026-03-02 22:01:12 +05:30
parent cb6a3a8042
commit a7bf355703
5 changed files with 258 additions and 194 deletions

View File

@@ -3,202 +3,161 @@ import {
Box,
Paper,
Typography,
Avatar,
AvatarGroup,
Chip,
List,
ListItem,
ListItemAvatar,
ListItemText,
Divider,
IconButton,
Tooltip
Tooltip,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
Stack,
} from '@mui/material';
import {
Psychology as StrategyIcon,
Article as ContentIcon,
Search as SeoIcon,
Campaign as SocialIcon,
CompareArrows as CompetitorIcon,
Refresh as RefreshIcon,
MoreVert as MoreVertIcon
ExpandMore as ExpandMoreIcon,
} from '@mui/icons-material';
import { apiClient } from '../../../api/client';
interface AgentStatus {
id: string;
name: string;
role: string;
status: 'active' | 'thinking' | 'idle' | 'offline';
current_activity: string;
icon: React.ElementType;
color: string;
}
type EventPayload = {
phase?: string | null;
step?: string | null;
tool_name?: string | null;
progress_percent?: number | null;
input_summary?: string | null;
output_summary?: string | null;
decision_reason?: string | null;
evidence_refs?: string[] | null;
safe_debug?: boolean;
metadata?: Record<string, unknown>;
};
// Mock data - In real implementation, this would come from a backend endpoint
// /api/agents/status or similar
const AGENT_TEAM: AgentStatus[] = [
{
id: 'strategy_architect',
name: 'Strategy Architect',
role: 'Team Lead',
status: 'active',
current_activity: 'Analyzing content pillar performance',
icon: StrategyIcon,
color: '#6366f1' // Indigo
},
{
id: 'content_strategist',
name: 'Content Strategist',
role: 'Creative',
status: 'thinking',
current_activity: 'Identifying semantic gaps in "AI Tools"',
icon: ContentIcon,
color: '#10b981' // Emerald
},
{
id: 'seo_specialist',
name: 'SEO Specialist',
role: 'Technical',
status: 'idle',
current_activity: 'Monitoring SERP rankings',
icon: SeoIcon,
color: '#f59e0b' // Amber
},
{
id: 'social_manager',
name: 'Social Manager',
role: 'Engagement',
status: 'idle',
current_activity: 'Waiting for new content to schedule',
icon: SocialIcon,
color: '#ec4899' // Pink
},
{
id: 'competitor_analyst',
name: 'Competitor Analyst',
role: 'Intelligence',
status: 'active',
current_activity: 'Scanning competitor X for new posts',
icon: CompetitorIcon,
color: '#ef4444' // Red
}
];
type TeamActivityEvent = {
id: number;
event_type: string;
severity: string;
message?: string | null;
created_at?: string | null;
payload?: EventPayload | null;
};
type AgentRun = {
id: number;
agent_type: string;
status: string;
started_at?: string | null;
};
const TeamHuddleWidget: React.FC = () => {
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [timeline, setTimeline] = React.useState<Array<{ run: AgentRun; events: TeamActivityEvent[] }>>([]);
const loadTimeline = React.useCallback(async () => {
setLoading(true);
setError(null);
try {
const runsResp = await apiClient.get('/api/agents/runs', { params: { limit: 5 } });
const runs: AgentRun[] = runsResp?.data?.data?.runs || [];
const eventResponses = await Promise.all(
runs.slice(0, 3).map(async (run) => {
const eventsResp = await apiClient.get(`/api/agents/runs/${run.id}/events`, { params: { limit: 25 } });
return {
run,
events: (eventsResp?.data?.data?.events || []) as TeamActivityEvent[],
};
}),
);
setTimeline(eventResponses);
} catch (e: any) {
setError(e?.message || 'Failed to load team activity');
} finally {
setLoading(false);
}
}, []);
React.useEffect(() => {
loadTimeline();
}, [loadTimeline]);
return (
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
height: '100%',
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)'
}}
>
<Paper elevation={0} sx={{ p: 2, borderRadius: 3, border: '1px solid', borderColor: 'divider', height: '100%' }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6" fontWeight={700} color="text.primary">
Team Huddle
</Typography>
<Chip
label="Live"
size="small"
color="success"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
/>
</Box>
<Box>
<Tooltip title="Refresh Team Status">
<IconButton size="small">
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
<Typography variant="h6" fontWeight={700}>Team Activity</Typography>
<Chip label="Live" size="small" color="success" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
</Box>
<Tooltip title="Refresh Team Activity">
<IconButton size="small" onClick={loadTimeline}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<List disablePadding>
{AGENT_TEAM.map((agent, index) => (
<React.Fragment key={agent.id}>
{index > 0 && <Divider variant="inset" component="li" sx={{ my: 1, ml: 7 }} />}
<ListItem
alignItems="flex-start"
disableGutters
sx={{ py: 0.5 }}
secondaryAction={
<Tooltip title={agent.status}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor:
agent.status === 'active' ? '#22c55e' :
agent.status === 'thinking' ? '#3b82f6' :
'#94a3b8',
boxShadow: agent.status === 'active' ? '0 0 0 2px rgba(34, 197, 94, 0.2)' : 'none',
animation: agent.status === 'thinking' ? 'pulse 1.5s infinite' : 'none',
'@keyframes pulse': {
'0%': { opacity: 1, transform: 'scale(1)' },
'50%': { opacity: 0.6, transform: 'scale(1.2)' },
'100%': { opacity: 1, transform: 'scale(1)' }
}
}}
/>
</Tooltip>
}
>
<ListItemAvatar>
<Avatar
sx={{
bgcolor: `${agent.color}15`,
color: agent.color,
width: 40,
height: 40
}}
>
<agent.icon fontSize="small" />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="subtitle2" fontWeight={600}>
{agent.name}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem', border: '1px solid #e2e8f0', px: 0.5, borderRadius: 1 }}>
{agent.role}
</Typography>
</Box>
}
secondary={
<Typography
variant="body2"
color="text.secondary"
sx={{
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontSize: '0.75rem',
mt: 0.25
}}
>
{agent.current_activity}
{loading && (
<Box py={4} textAlign="center">
<CircularProgress size={24} />
</Box>
)}
{!loading && error && (
<Typography variant="body2" color="error">{error}</Typography>
)}
{!loading && !error && timeline.length === 0 && (
<Typography variant="body2" color="text.secondary">No team activity yet.</Typography>
)}
{!loading && !error && timeline.length > 0 && (
<List disablePadding>
{timeline.map(({ run, events }, index) => (
<React.Fragment key={run.id}>
{index > 0 && <Divider sx={{ my: 1 }} />}
<ListItem disableGutters sx={{ display: 'block' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
<Chip size="small" label={run.agent_type || 'Agent'} />
<Chip size="small" color={run.status === 'completed' ? 'success' : 'warning'} label={run.status} />
<Typography variant="caption" color="text.secondary">
{run.started_at ? new Date(run.started_at).toLocaleString() : ''}
</Typography>
}
/>
</ListItem>
</React.Fragment>
))}
</List>
<Box mt={2} pt={2} borderTop="1px solid #eee" display="flex" justifyContent="center">
<Typography variant="caption" color="primary" sx={{ fontWeight: 600, cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}>
View Full Team Activity
</Typography>
</Box>
</Stack>
{events.map((event) => {
const payload = event.payload || {};
return (
<Accordion key={event.id} disableGutters elevation={0} sx={{ border: '1px solid #e5e7eb', mb: 1 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ width: '100%' }}>
<Chip size="small" label={payload.phase || event.event_type} />
{payload.step && <Chip size="small" variant="outlined" label={payload.step} />}
<Typography variant="body2" sx={{ flexGrow: 1 }}>
{event.message || payload.output_summary || 'Activity update'}
</Typography>
{typeof payload.progress_percent === 'number' && (
<Typography variant="caption" color="text.secondary">{payload.progress_percent}%</Typography>
)}
</Stack>
</AccordionSummary>
<AccordionDetails>
<Typography variant="caption" display="block">Tool: {payload.tool_name || '—'}</Typography>
<Typography variant="caption" display="block">Input: {payload.input_summary || '—'}</Typography>
<Typography variant="caption" display="block">Output: {payload.output_summary || '—'}</Typography>
<Typography variant="caption" display="block">Decision: {payload.decision_reason || '—'}</Typography>
<Typography variant="caption" display="block">Evidence: {(payload.evidence_refs || []).join(', ') || '—'}</Typography>
<Typography variant="caption" display="block">Safe debug: {String(payload.safe_debug ?? true)}</Typography>
</AccordionDetails>
</Accordion>
);
})}
</ListItem>
</React.Fragment>
))}
</List>
)}
</Paper>
);
};