feat: load team huddle agents from API endpoints

This commit is contained in:
ي
2026-03-02 21:48:50 +05:30
parent cb6a3a8042
commit 124de1379a

View File

@@ -4,7 +4,6 @@ import {
Paper, Paper,
Typography, Typography,
Avatar, Avatar,
AvatarGroup,
Chip, Chip,
List, List,
ListItem, ListItem,
@@ -12,7 +11,8 @@ import {
ListItemText, ListItemText,
Divider, Divider,
IconButton, IconButton,
Tooltip Tooltip,
CircularProgress
} from '@mui/material'; } from '@mui/material';
import { import {
Psychology as StrategyIcon, Psychology as StrategyIcon,
@@ -20,11 +20,13 @@ import {
Search as SeoIcon, Search as SeoIcon,
Campaign as SocialIcon, Campaign as SocialIcon,
CompareArrows as CompetitorIcon, CompareArrows as CompetitorIcon,
SmartToy as AgentIcon,
Refresh as RefreshIcon, Refresh as RefreshIcon,
MoreVert as MoreVertIcon WarningAmber as WarningIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { apiClient } from '../../../api/client';
interface AgentStatus { interface AgentCard {
id: string; id: string;
name: string; name: string;
role: string; role: string;
@@ -34,57 +36,159 @@ interface AgentStatus {
color: string; color: string;
} }
// Mock data - In real implementation, this would come from a backend endpoint interface AgentMeta {
// /api/agents/status or similar name: string;
const AGENT_TEAM: AgentStatus[] = [ role: string;
{ icon: React.ElementType;
id: 'strategy_architect', color: string;
}
const AGENT_META: Record<string, AgentMeta> = {
strategy: {
name: 'Strategy Architect', name: 'Strategy Architect',
role: 'Team Lead', role: 'Team Lead',
status: 'active',
current_activity: 'Analyzing content pillar performance',
icon: StrategyIcon, icon: StrategyIcon,
color: '#6366f1' // Indigo color: '#6366f1'
}, },
{ content: {
id: 'content_strategist',
name: 'Content Strategist', name: 'Content Strategist',
role: 'Creative', role: 'Creative',
status: 'thinking',
current_activity: 'Identifying semantic gaps in "AI Tools"',
icon: ContentIcon, icon: ContentIcon,
color: '#10b981' // Emerald color: '#10b981'
}, },
{ seo: {
id: 'seo_specialist',
name: 'SEO Specialist', name: 'SEO Specialist',
role: 'Technical', role: 'Technical',
status: 'idle',
current_activity: 'Monitoring SERP rankings',
icon: SeoIcon, icon: SeoIcon,
color: '#f59e0b' // Amber color: '#f59e0b'
}, },
{ social: {
id: 'social_manager',
name: 'Social Manager', name: 'Social Manager',
role: 'Engagement', role: 'Engagement',
status: 'idle',
current_activity: 'Waiting for new content to schedule',
icon: SocialIcon, icon: SocialIcon,
color: '#ec4899' // Pink color: '#ec4899'
}, },
{ competitor: {
id: 'competitor_analyst',
name: 'Competitor Analyst', name: 'Competitor Analyst',
role: 'Intelligence', role: 'Intelligence',
status: 'active',
current_activity: 'Scanning competitor X for new posts',
icon: CompetitorIcon, icon: CompetitorIcon,
color: '#ef4444' // Red color: '#ef4444'
} }
]; };
const normalizeAgentKey = (rawKey: string): string => {
const key = (rawKey || '').toLowerCase();
if (key.includes('strategy')) return 'strategy';
if (key.includes('content')) return 'content';
if (key.includes('seo') || key.includes('search')) return 'seo';
if (key.includes('social')) return 'social';
if (key.includes('competitor') || key.includes('competition')) return 'competitor';
return key;
};
const normalizeStatus = (status: string): AgentCard['status'] => {
const value = (status || '').toLowerCase();
if (value === 'active' || value === 'thinking' || value === 'idle' || value === 'offline') {
return value;
}
if (value === 'running' || value === 'busy') return 'active';
if (value === 'processing' || value === 'analyzing') return 'thinking';
if (value === 'inactive' || value === 'stopped') return 'offline';
return 'idle';
};
const TeamHuddleWidget: React.FC = () => { const TeamHuddleWidget: React.FC = () => {
const [agents, setAgents] = React.useState<AgentCard[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [refreshing, setRefreshing] = React.useState(false);
const [alertCount, setAlertCount] = React.useState(0);
const fetchTeamData = React.useCallback(async (isRefresh = false) => {
if (isRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
try {
const [statusResp, runsResp, alertsResp] = await Promise.allSettled([
apiClient.get('/api/agents/status'),
apiClient.get('/api/agents/runs', { params: { limit: 20 } }),
apiClient.get('/api/agents/alerts', { params: { unread_only: true, limit: 20 } })
]);
if (statusResp.status !== 'fulfilled') {
throw new Error('Unable to load team status');
}
const statusPayload = statusResp.value?.data?.data?.agents || statusResp.value?.data?.agents || [];
const runsPayload = runsResp.status === 'fulfilled'
? (runsResp.value?.data?.data?.runs || runsResp.value?.data?.runs || [])
: [];
const alertsPayload = alertsResp.status === 'fulfilled'
? (alertsResp.value?.data?.data?.alerts || alertsResp.value?.data?.alerts || [])
: [];
const runByAgent = new Map<string, string>();
runsPayload.forEach((run: Record<string, unknown>) => {
const key = normalizeAgentKey(run.agent_key || run.agent || run.agent_id || '');
if (!key || runByAgent.has(key)) return;
runByAgent.set(
key,
run.summary || run.context || run.activity || run.status_message || 'Working on recent tasks'
);
});
const normalizedAgents: AgentCard[] = statusPayload.map((agent: Record<string, unknown>, index: number) => {
const rawKey = agent.key || agent.agent_key || agent.id || agent.name || `agent-${index}`;
const normalizedKey = normalizeAgentKey(rawKey);
const meta = AGENT_META[normalizedKey] || {
name: agent.display_name || agent.name || rawKey,
role: 'AI Agent',
icon: AgentIcon,
color: '#64748b'
};
return {
id: String(rawKey),
name: agent.display_name || meta.name,
role: agent.role || meta.role,
status: normalizeStatus(agent.status || agent.operational_status || ''),
current_activity:
agent.current_activity ||
runByAgent.get(normalizedKey) ||
agent.last_message ||
'Waiting for next instruction',
icon: meta.icon,
color: meta.color
};
});
setAgents(normalizedAgents);
setAlertCount(Array.isArray(alertsPayload) ? alertsPayload.length : 0);
} catch (err) {
console.error('Failed to load team huddle data:', err);
setError('Could not load team huddle data. Please try again.');
setAgents([]);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
React.useEffect(() => {
fetchTeamData();
}, [fetchTeamData]);
return ( return (
<Paper <Paper
elevation={0} elevation={0}
@@ -102,40 +206,77 @@ const TeamHuddleWidget: React.FC = () => {
<Typography variant="h6" fontWeight={700} color="text.primary"> <Typography variant="h6" fontWeight={700} color="text.primary">
Team Huddle Team Huddle
</Typography> </Typography>
<Chip <Chip
label="Live" label="Live"
size="small" size="small"
color="success" color="success"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
/> />
{alertCount > 0 && (
<Chip
icon={<WarningIcon fontSize="small" />}
label={`${alertCount} alert${alertCount > 1 ? 's' : ''}`}
size="small"
color="warning"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
/>
)}
</Box> </Box>
<Box> <Box>
<Tooltip title="Refresh Team Status"> <Tooltip title="Refresh Team Status">
<IconButton size="small"> <span>
<RefreshIcon fontSize="small" /> <IconButton size="small" onClick={() => fetchTeamData(true)} disabled={loading || refreshing}>
</IconButton> {refreshing ? <CircularProgress size={16} /> : <RefreshIcon fontSize="small" />}
</IconButton>
</span>
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
<List disablePadding> {loading ? (
{AGENT_TEAM.map((agent, index) => ( <Box display="flex" justifyContent="center" alignItems="center" minHeight={180}>
<React.Fragment key={agent.id}> <CircularProgress size={24} />
{index > 0 && <Divider variant="inset" component="li" sx={{ my: 1, ml: 7 }} />} </Box>
<ListItem ) : error ? (
alignItems="flex-start" <Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" minHeight={180} gap={1}>
disableGutters <Typography variant="body2" color="error.main" textAlign="center">
sx={{ py: 0.5 }} {error}
secondaryAction={ </Typography>
<Tooltip title={agent.status}> <Typography
variant="caption"
color="primary"
sx={{ fontWeight: 600, cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
onClick={() => fetchTeamData(true)}
>
Retry
</Typography>
</Box>
) : agents.length === 0 ? (
<Box display="flex" justifyContent="center" alignItems="center" minHeight={180}>
<Typography variant="body2" color="text.secondary" textAlign="center">
No active agent activity right now.
</Typography>
</Box>
) : (
<List disablePadding>
{agents.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 <Box
sx={{ sx={{
width: 8, width: 8,
height: 8, height: 8,
borderRadius: '50%', borderRadius: '50%',
bgcolor: bgcolor:
agent.status === 'active' ? '#22c55e' : agent.status === 'active' ? '#22c55e' :
agent.status === 'thinking' ? '#3b82f6' : agent.status === 'thinking' ? '#3b82f6' :
agent.status === 'offline' ? '#cbd5e1' :
'#94a3b8', '#94a3b8',
boxShadow: agent.status === 'active' ? '0 0 0 2px rgba(34, 197, 94, 0.2)' : 'none', boxShadow: agent.status === 'active' ? '0 0 0 2px rgba(34, 197, 94, 0.2)' : 'none',
animation: agent.status === 'thinking' ? 'pulse 1.5s infinite' : 'none', animation: agent.status === 'thinking' ? 'pulse 1.5s infinite' : 'none',
@@ -146,54 +287,55 @@ const TeamHuddleWidget: React.FC = () => {
} }
}} }}
/> />
</Tooltip> </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 <ListItemAvatar>
variant="body2" <Avatar
color="text.secondary"
sx={{ sx={{
display: '-webkit-box', bgcolor: `${agent.color}15`,
WebkitLineClamp: 1, color: agent.color,
WebkitBoxOrient: 'vertical', width: 40,
overflow: 'hidden', height: 40
fontSize: '0.75rem',
mt: 0.25
}} }}
> >
{agent.current_activity} <agent.icon fontSize="small" />
</Typography> </Avatar>
} </ListItemAvatar>
/> <ListItemText
</ListItem> primary={
</React.Fragment> <Box display="flex" alignItems="center" gap={1}>
))} <Typography variant="subtitle2" fontWeight={600}>
</List> {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"> <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 variant="caption" color="primary" sx={{ fontWeight: 600, cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}>
View Full Team Activity View Full Team Activity