feat: load team huddle agents from API endpoints
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user