feat: load team huddle agents from API endpoints
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
Paper,
|
||||
Typography,
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
Chip,
|
||||
List,
|
||||
ListItem,
|
||||
@@ -12,7 +11,8 @@ import {
|
||||
ListItemText,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Psychology as StrategyIcon,
|
||||
@@ -20,11 +20,13 @@ import {
|
||||
Search as SeoIcon,
|
||||
Campaign as SocialIcon,
|
||||
CompareArrows as CompetitorIcon,
|
||||
SmartToy as AgentIcon,
|
||||
Refresh as RefreshIcon,
|
||||
MoreVert as MoreVertIcon
|
||||
WarningAmber as WarningIcon
|
||||
} from '@mui/icons-material';
|
||||
import { apiClient } from '../../../api/client';
|
||||
|
||||
interface AgentStatus {
|
||||
interface AgentCard {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
@@ -34,57 +36,159 @@ interface AgentStatus {
|
||||
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',
|
||||
interface AgentMeta {
|
||||
name: string;
|
||||
role: string;
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const AGENT_META: Record<string, AgentMeta> = {
|
||||
strategy: {
|
||||
name: 'Strategy Architect',
|
||||
role: 'Team Lead',
|
||||
status: 'active',
|
||||
current_activity: 'Analyzing content pillar performance',
|
||||
icon: StrategyIcon,
|
||||
color: '#6366f1' // Indigo
|
||||
color: '#6366f1'
|
||||
},
|
||||
{
|
||||
id: 'content_strategist',
|
||||
content: {
|
||||
name: 'Content Strategist',
|
||||
role: 'Creative',
|
||||
status: 'thinking',
|
||||
current_activity: 'Identifying semantic gaps in "AI Tools"',
|
||||
icon: ContentIcon,
|
||||
color: '#10b981' // Emerald
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
id: 'seo_specialist',
|
||||
seo: {
|
||||
name: 'SEO Specialist',
|
||||
role: 'Technical',
|
||||
status: 'idle',
|
||||
current_activity: 'Monitoring SERP rankings',
|
||||
icon: SeoIcon,
|
||||
color: '#f59e0b' // Amber
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
id: 'social_manager',
|
||||
social: {
|
||||
name: 'Social Manager',
|
||||
role: 'Engagement',
|
||||
status: 'idle',
|
||||
current_activity: 'Waiting for new content to schedule',
|
||||
icon: SocialIcon,
|
||||
color: '#ec4899' // Pink
|
||||
color: '#ec4899'
|
||||
},
|
||||
{
|
||||
id: 'competitor_analyst',
|
||||
competitor: {
|
||||
name: 'Competitor Analyst',
|
||||
role: 'Intelligence',
|
||||
status: 'active',
|
||||
current_activity: 'Scanning competitor X for new posts',
|
||||
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 [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 (
|
||||
<Paper
|
||||
elevation={0}
|
||||
@@ -102,40 +206,77 @@ const TeamHuddleWidget: React.FC = () => {
|
||||
<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 }}
|
||||
<Chip
|
||||
label="Live"
|
||||
size="small"
|
||||
color="success"
|
||||
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>
|
||||
<Tooltip title="Refresh Team Status">
|
||||
<IconButton size="small">
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Tooltip title="Refresh Team Status">
|
||||
<span>
|
||||
<IconButton size="small" onClick={() => fetchTeamData(true)} disabled={loading || refreshing}>
|
||||
{refreshing ? <CircularProgress size={16} /> : <RefreshIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</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}>
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight={180}>
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" minHeight={180} gap={1}>
|
||||
<Typography variant="body2" color="error.main" textAlign="center">
|
||||
{error}
|
||||
</Typography>
|
||||
<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
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor:
|
||||
agent.status === 'active' ? '#22c55e' :
|
||||
agent.status === 'thinking' ? '#3b82f6' :
|
||||
bgcolor:
|
||||
agent.status === 'active' ? '#22c55e' :
|
||||
agent.status === 'thinking' ? '#3b82f6' :
|
||||
agent.status === 'offline' ? '#cbd5e1' :
|
||||
'#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',
|
||||
@@ -146,54 +287,55 @@ const TeamHuddleWidget: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</Tooltip>
|
||||
}
|
||||
secondary={
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 1,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
fontSize: '0.75rem',
|
||||
mt: 0.25
|
||||
bgcolor: `${agent.color}15`,
|
||||
color: agent.color,
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
>
|
||||
{agent.current_activity}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
</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
|
||||
|
||||
Reference in New Issue
Block a user