Merge branch 'review/pr-365'

This commit is contained in:
ajaysi
2026-03-03 09:09:07 +05:30

View File

@@ -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