Add agent huddle SSE feed with frontend live subscriptions

This commit is contained in:
ي
2026-03-02 22:00:24 +05:30
parent cb6a3a8042
commit c0d9289d4d
5 changed files with 522 additions and 156 deletions

View File

@@ -4,7 +4,6 @@ import {
Paper,
Typography,
Avatar,
AvatarGroup,
Chip,
List,
ListItem,
@@ -20,182 +19,70 @@ import {
Search as SeoIcon,
Campaign as SocialIcon,
CompareArrows as CompetitorIcon,
Refresh as RefreshIcon,
MoreVert as MoreVertIcon
Refresh as RefreshIcon
} from '@mui/icons-material';
import { Link as RouterLink } from 'react-router-dom';
import { useAgentHuddleFeed } from '../../../hooks/useAgentHuddleFeed';
interface AgentStatus {
id: string;
name: string;
role: string;
status: 'active' | 'thinking' | 'idle' | 'offline';
current_activity: string;
icon: React.ElementType;
color: string;
}
// 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
}
];
const ICON_BY_AGENT: Record<string, React.ElementType> = {
strategy: StrategyIcon,
content: ContentIcon,
seo: SeoIcon,
social: SocialIcon,
competitor: CompetitorIcon,
};
const TeamHuddleWidget: React.FC = () => {
const { runs, events, connectionMode, lastHeartbeatAt } = useAgentHuddleFeed();
const rows = React.useMemo(() => {
return runs.slice(0, 5).map((run) => {
const event = events.find((e) => e.agent_type === run.agent_type);
const agentType = String(run.agent_type || 'strategy');
const IconComponent = ICON_BY_AGENT[agentType] || StrategyIcon;
const status = run.status === 'running' ? 'thinking' : run.success === false ? 'offline' : 'active';
return {
id: run.id,
name: agentType.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
role: run.status || 'active',
status,
current_activity: event?.message || run.result_summary || 'Awaiting next update',
icon: IconComponent,
};
});
}, [runs, events]);
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%', background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 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} color="text.primary">Team Huddle</Typography>
<Chip label={connectionMode === 'sse' ? 'Live' : connectionMode === 'polling' ? 'Polling' : 'Connecting'} size="small" color={connectionMode === 'sse' ? 'success' : 'warning'} sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
</Box>
<Tooltip title={lastHeartbeatAt ? `Heartbeat ${new Date(lastHeartbeatAt).toLocaleTimeString()}` : 'Waiting for heartbeat'}>
<IconButton size="small"><RefreshIcon fontSize="small" /></IconButton>
</Tooltip>
</Box>
<List disablePadding>
{AGENT_TEAM.map((agent, index) => (
{rows.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>
}
>
<ListItem alignItems="flex-start" disableGutters sx={{ py: 0.5 }}>
<ListItemAvatar>
<Avatar
sx={{
bgcolor: `${agent.color}15`,
color: agent.color,
width: 40,
height: 40
}}
>
<agent.icon fontSize="small" />
</Avatar>
<Avatar sx={{ bgcolor: '#eef2ff', color: '#6366f1', 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}
</Typography>
}
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}</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' } }}>
<Typography component={RouterLink} to="/team-activity" variant="caption" color="primary" sx={{ fontWeight: 600, textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}>
View Full Team Activity
</Typography>
</Box>