Update frontend components: TeamHuddleWidget, useAgentHuddleFeed, TeamActivityPage
This commit is contained in:
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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": "/"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user