Update frontend components: TeamHuddleWidget, useAgentHuddleFeed, TeamActivityPage

This commit is contained in:
ajaysi
2026-03-04 09:19:53 +05:30
parent 45fb9636e2
commit 1d36ebe2f9
6 changed files with 43 additions and 185 deletions

View File

@@ -46,7 +46,7 @@
"@types/jest": "^30.0.0",
"@types/node": "^25.0.10",
"source-map-explorer": "^2.5.2",
"typescript": "^4.9.5"
"typescript": "^5.3.3"
}
},
"node_modules/@0no-co/graphql.web": {
@@ -24057,16 +24057,16 @@
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {

View File

@@ -68,7 +68,7 @@
"@types/jest": "^30.0.0",
"@types/node": "^25.0.10",
"source-map-explorer": "^2.5.2",
"typescript": "^4.9.5"
"typescript": "^5.3.3"
},
"proxy": "http://localhost:8000",
"homepage": "/"

View File

@@ -51,7 +51,6 @@ import BillingPage from './pages/BillingPage';
import ApprovalsPage from './pages/ApprovalsPage';
import TeamActivityPage from './pages/TeamActivityPage';
import StripeDisputesDashboard from './pages/StripeDisputesDashboard';
import TeamActivityPage from './pages/TeamActivityPage';
import ProtectedRoute from './components/shared/ProtectedRoute';
import GSCAuthCallback from './components/SEODashboard/components/GSCAuthCallback';
import Landing from './components/Landing/Landing';

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import {
Box,
Paper,
@@ -19,7 +19,7 @@ import {
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
} from '@mui/icons-material';
import { apiClient } from '../../../api/client';
import { useAgentHuddleFeed } from '../../../hooks/useAgentHuddleFeed';
type EventPayload = {
phase?: string | null;
@@ -34,68 +34,39 @@ type EventPayload = {
metadata?: Record<string, unknown>;
};
type TeamActivityEvent = {
id: number;
event_type: string;
severity: string;
message?: string | null;
created_at?: string | null;
payload?: EventPayload | null;
};
type AgentRun = {
id: number;
agent_type: string;
status: string;
started_at?: string | null;
};
const TeamHuddleWidget: React.FC = () => {
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [timeline, setTimeline] = React.useState<Array<{ run: AgentRun; events: TeamActivityEvent[] }>>([]);
const { runs, events, connectionMode } = useAgentHuddleFeed({ detailTier: 'detailed' });
const loadTimeline = React.useCallback(async () => {
setLoading(true);
setError(null);
try {
const runsResp = await apiClient.get('/api/agents/runs', { params: { limit: 5 } });
const runs: AgentRun[] = runsResp?.data?.data?.runs || [];
// Group events by run_id for the UI
const timeline = useMemo(() => {
// Only take top 5 runs
const topRuns = runs.slice(0, 5);
return topRuns.map(run => {
// Find events for this run
const runEvents = events
.filter(e => e.run_id === run.id)
.map(e => ({
...e,
payload: (e.payload || {}) as EventPayload
}));
return { run, events: runEvents };
});
}, [runs, events]);
const eventResponses = await Promise.all(
runs.slice(0, 3).map(async (run) => {
const eventsResp = await apiClient.get(`/api/agents/runs/${run.id}/events`, { params: { limit: 25 } });
return {
run,
events: (eventsResp?.data?.data?.events || []) as TeamActivityEvent[],
};
}),
);
setTimeline(eventResponses);
} catch (e: any) {
setError(e?.message || 'Failed to load team activity');
} finally {
setLoading(false);
}
}, []);
React.useEffect(() => {
loadTimeline();
}, [loadTimeline]);
const loading = connectionMode === 'connecting' && runs.length === 0;
return (
<Paper elevation={0} sx={{ p: 2, borderRadius: 3, border: '1px solid', borderColor: 'divider', height: '100%' }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6" fontWeight={700}>Team Activity</Typography>
<Chip label="Live" size="small" color="success" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
<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="Refresh Team Activity">
<IconButton size="small" onClick={loadTimeline}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
{loading && (
@@ -104,15 +75,11 @@ const TeamHuddleWidget: React.FC = () => {
</Box>
)}
{!loading && error && (
<Typography variant="body2" color="error">{error}</Typography>
)}
{!loading && !error && timeline.length === 0 && (
{!loading && timeline.length === 0 && (
<Typography variant="body2" color="text.secondary">No team activity yet.</Typography>
)}
{!loading && !error && timeline.length > 0 && (
{!loading && timeline.length > 0 && (
<List disablePadding>
{timeline.map(({ run, events }, index) => (
<React.Fragment key={run.id}>

View File

@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { apiClient, getApiUrl, getAuthTokenGetter } from '../api/client';
export interface AgentRunItem { id: number; agent_type?: string; status?: string; success?: boolean | null; result_summary?: string | null; finished_at?: string | null; }
export interface AgentEventItem { id: number; agent_type?: string; event_type?: string; message?: string | null; created_at?: string | null; }
export interface AgentAlertItem { id: number; title?: string; message?: string; severity?: string; read_at?: string | null; }
export interface AgentRunItem { id: number; agent_type?: string; status?: string; success?: boolean | null; result_summary?: string | null; error_message?: string | null; finished_at?: string | null; started_at?: string | null; }
export interface AgentEventItem { id: number; run_id?: number; agent_type?: string; event_type?: string; message?: string | null; created_at?: string | null; payload?: any; }
export interface AgentAlertItem { id: number; title?: string; message?: string; severity?: string; read_at?: string | null; payload?: any; }
export interface AgentApprovalItem { id: number; action_type?: string; status?: string; risk_level?: number; created_at?: string | null; }
interface Cursor { run_id: number; event_id: number; alert_id: number; approval_id: number; }
@@ -42,13 +42,14 @@ const parseSseLines = (raw: string): Array<{ event: string; data: string }> => {
});
};
export const useAgentHuddleFeed = () => {
export const useAgentHuddleFeed = (options?: { detailTier?: 'summary' | 'detailed' | 'debug' }) => {
const [feed, setFeed] = useState<FeedPayload>({ runs: [], events: [], alerts: [], approvals: [], cursor: DEFAULT_CURSOR });
const [connectionMode, setConnectionMode] = useState<'connecting' | 'sse' | 'polling'>('connecting');
const [lastHeartbeatAt, setLastHeartbeatAt] = useState<number | null>(null);
const stopRef = useRef(false);
const reconnectAttemptRef = useRef(0);
const cursorRef = useRef<Cursor>(DEFAULT_CURSOR);
const detailTier = options?.detailTier || 'summary';
const applyPayload = useCallback((payload: Partial<FeedPayload>, replace = false) => {
setFeed((prev) => ({
@@ -74,11 +75,12 @@ export const useAgentHuddleFeed = () => {
}, []);
const loadSnapshot = useCallback(async (cursor?: Cursor) => {
const resp = await apiClient.get('/api/agents/huddle/feed', { params: cursor || {} });
const params = { ...(cursor || {}), detail_tier: detailTier };
const resp = await apiClient.get('/api/agents/huddle/feed', { params });
const data = resp?.data?.data as FeedPayload;
applyPayload(data, !cursor);
return data;
}, [applyPayload]);
}, [applyPayload, detailTier]);
useEffect(() => {
stopRef.current = false;
@@ -104,7 +106,8 @@ export const useAgentHuddleFeed = () => {
const token = tokenGetter ? await tokenGetter() : null;
if (!token) throw new Error('No auth token available for SSE stream');
const response = await fetch(`${getApiUrl()}/api/agents/huddle/stream`, {
const streamUrl = `${getApiUrl()}/api/agents/huddle/stream?detail_tier=${detailTier}`;
const response = await fetch(streamUrl, {
headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' },
});

View File

@@ -1,114 +1,4 @@
import React from 'react';
<<<<<<< HEAD
import {
Box,
Chip,
Divider,
Paper,
Stack,
Typography,
List,
ListItem,
ListItemText,
} from '@mui/material';
const activeRuns = [
{ id: 'run-9142', title: 'Q4 SEO Pillar Refresh', owner: 'SEO Specialist', progress: '67%' },
{ id: 'run-9143', title: 'LinkedIn Carousel Draft', owner: 'Content Strategist', progress: '42%' },
{ id: 'run-9144', title: 'Competitor Monitoring Sweep', owner: 'Competitor Analyst', progress: '88%' },
];
const timelineEvents = [
{ time: '09:20', detail: 'Strategy Architect approved updated campaign goals.' },
{ time: '09:35', detail: 'Social Manager queued 6 posts for review.' },
{ time: '10:05', detail: 'SEO Specialist flagged ranking drop on "AI copy tools".' },
{ time: '10:24', detail: 'Content Strategist generated new briefing packet.' },
];
const alerts = [
{ severity: 'high', label: '2 content briefs are blocked on missing references.' },
{ severity: 'medium', label: 'SERP volatility increased for 3 tracked keywords.' },
];
const approvals = [
{ id: 'ap-112', action: 'Publish LinkedIn thread for Campaign Alpha', requestedBy: 'Social Manager' },
{ id: 'ap-113', action: 'Approve budget reallocation to ad creatives', requestedBy: 'Strategy Architect' },
];
export default function TeamActivityPage() {
return (
<Box sx={{ p: { xs: 2, md: 3 } }}>
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1 }}>
Team Activity
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Real-time view of active agent workstreams, timelines, alerts, and pending approvals.
</Typography>
<Stack spacing={2.5}>
<Paper sx={{ p: 2.5 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Active Runs</Typography>
<Stack spacing={1}>
{activeRuns.map((run) => (
<Stack key={run.id} direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography sx={{ fontWeight: 600 }}>{run.title}</Typography>
<Typography variant="caption" color="text.secondary">{run.owner}</Typography>
</Box>
<Chip size="small" label={run.progress} color="primary" variant="outlined" />
</Stack>
))}
</Stack>
</Paper>
<Paper sx={{ p: 2.5 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Event Timeline</Typography>
<List disablePadding>
{timelineEvents.map((event, index) => (
<React.Fragment key={`${event.time}-${event.detail}`}>
{index > 0 && <Divider sx={{ my: 1 }} />}
<ListItem disableGutters sx={{ py: 0 }}>
<ListItemText
primary={<Typography sx={{ fontWeight: 600 }}>{event.detail}</Typography>}
secondary={<Typography variant="caption">{event.time}</Typography>}
/>
</ListItem>
</React.Fragment>
))}
</List>
</Paper>
<Paper sx={{ p: 2.5 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Alerts</Typography>
<Stack spacing={1}>
{alerts.map((alert) => (
<Chip
key={alert.label}
label={alert.label}
color={alert.severity === 'high' ? 'error' : 'warning'}
variant="outlined"
sx={{ justifyContent: 'flex-start' }}
/>
))}
</Stack>
</Paper>
<Paper sx={{ p: 2.5 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1.5 }}>Approvals Requiring Action</Typography>
<Stack spacing={1}>
{approvals.map((approval) => (
<Box key={approval.id}>
<Typography sx={{ fontWeight: 600 }}>{approval.action}</Typography>
<Typography variant="caption" color="text.secondary">Requested by {approval.requestedBy}</Typography>
</Box>
))}
</Stack>
</Paper>
</Stack>
</Box>
);
}
=======
import { Box, Card, CardContent, Chip, Divider, Grid, List, ListItem, ListItemText, Typography } from '@mui/material';
import { useAgentHuddleFeed } from '../hooks/useAgentHuddleFeed';
@@ -174,4 +64,3 @@ const TeamActivityPage: React.FC = () => {
};
export default TeamActivityPage;
>>>>>>> pr-368