Standardize agent event payloads and team activity timeline UI

This commit is contained in:
ي
2026-03-02 22:01:12 +05:30
parent cb6a3a8042
commit a7bf355703
5 changed files with 258 additions and 194 deletions

View File

@@ -1,13 +1,81 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from models.agent_activity_models import AgentAlert, AgentApprovalRequest, AgentEvent, AgentRun from models.agent_activity_models import AgentAlert, AgentApprovalRequest, AgentEvent, AgentRun
@dataclass
class AgentEventPayload:
"""Shared schema for agent activity event payloads."""
phase: Optional[str] = None
step: Optional[str] = None
tool_name: Optional[str] = None
progress_percent: Optional[float] = None
input_summary: Optional[str] = None
output_summary: Optional[str] = None
decision_reason: Optional[str] = None
evidence_refs: List[str] = field(default_factory=list)
safe_debug: bool = True
metadata: Dict[str, Any] = field(default_factory=dict)
def build_agent_event_payload(
*,
phase: Optional[str] = None,
step: Optional[str] = None,
tool_name: Optional[str] = None,
progress_percent: Optional[float] = None,
input_summary: Optional[str] = None,
output_summary: Optional[str] = None,
decision_reason: Optional[str] = None,
evidence_refs: Optional[List[str]] = None,
safe_debug: bool = True,
metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
return asdict(
AgentEventPayload(
phase=phase,
step=step,
tool_name=tool_name,
progress_percent=progress_percent,
input_summary=input_summary,
output_summary=output_summary,
decision_reason=decision_reason,
evidence_refs=list(evidence_refs or []),
safe_debug=bool(safe_debug),
metadata=dict(metadata or {}),
)
)
def _normalize_event_payload(payload: Optional[Union[Dict[str, Any], AgentEventPayload]]) -> Dict[str, Any]:
if payload is None:
return build_agent_event_payload()
if isinstance(payload, AgentEventPayload):
return asdict(payload)
if not isinstance(payload, dict):
return build_agent_event_payload(output_summary=str(payload)[:2000], safe_debug=False)
return build_agent_event_payload(
phase=payload.get("phase"),
step=payload.get("step"),
tool_name=payload.get("tool_name"),
progress_percent=payload.get("progress_percent"),
input_summary=payload.get("input_summary"),
output_summary=payload.get("output_summary"),
decision_reason=payload.get("decision_reason"),
evidence_refs=payload.get("evidence_refs") if isinstance(payload.get("evidence_refs"), list) else [],
safe_debug=bool(payload.get("safe_debug", True)),
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
class AgentActivityService: class AgentActivityService:
def __init__(self, db: Session, user_id: str): def __init__(self, db: Session, user_id: str):
self.db = db self.db = db
@@ -50,10 +118,11 @@ class AgentActivityService:
event_type: str, event_type: str,
severity: str = "info", severity: str = "info",
message: Optional[str] = None, message: Optional[str] = None,
payload: Optional[Dict[str, Any]] = None, payload: Optional[Union[Dict[str, Any], AgentEventPayload]] = None,
run_id: Optional[int] = None, run_id: Optional[int] = None,
agent_type: Optional[str] = None, agent_type: Optional[str] = None,
) -> AgentEvent: ) -> AgentEvent:
normalized_payload = _normalize_event_payload(payload)
evt = AgentEvent( evt = AgentEvent(
run_id=run_id, run_id=run_id,
user_id=self.user_id, user_id=self.user_id,
@@ -61,7 +130,7 @@ class AgentActivityService:
event_type=event_type, event_type=event_type,
severity=severity, severity=severity,
message=message, message=message,
payload=payload, payload=normalized_payload,
created_at=datetime.utcnow(), created_at=datetime.utcnow(),
) )
self.db.add(evt) self.db.add(evt)

View File

@@ -31,7 +31,7 @@ from utils.logger_utils import get_service_logger
from services.database import get_session_for_user from services.database import get_session_for_user
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
from services.intelligence.agents.safety_framework import get_safety_framework from services.intelligence.agents.safety_framework import get_safety_framework
from services.agent_activity_service import AgentActivityService from services.agent_activity_service import AgentActivityService, build_agent_event_payload
from services.intelligence.agents.agent_usage_tracking import track_agent_usage_sync from services.intelligence.agents.agent_usage_tracking import track_agent_usage_sync
import time import time
@@ -426,7 +426,7 @@ class BaseALwrityAgent(ABC):
event_type="plan", event_type="plan",
severity="info", severity="info",
message=(prompt[:2000] if prompt else None), message=(prompt[:2000] if prompt else None),
payload={"kind": "prompt"}, payload=build_agent_event_payload(phase="planning", step="run_started", tool_name="agent_run", progress_percent=0, input_summary=prompt[:250], output_summary="Agent run initialized", decision_reason="Received run prompt", safe_debug=False, metadata={"kind": "prompt"}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -453,7 +453,7 @@ class BaseALwrityAgent(ABC):
event_type="final_summary", event_type="final_summary",
severity="info", severity="info",
message=(str(result)[:2000] if result is not None else None), message=(str(result)[:2000] if result is not None else None),
payload={"kind": "result"}, payload=build_agent_event_payload(phase="execution", step="run_completed", tool_name="agent_run", progress_percent=100, output_summary=str(result)[:400] if result is not None else "No output", decision_reason="Run completed", safe_debug=True, metadata={"kind": "result"}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -467,7 +467,7 @@ class BaseALwrityAgent(ABC):
event_type="error", event_type="error",
severity="error", severity="error",
message=str(e)[:2000], message=str(e)[:2000],
payload={"kind": "exception"}, payload=build_agent_event_payload(phase="execution", step="run_error", tool_name="agent_runtime", output_summary=str(e)[:400], decision_reason="Unhandled exception during run", safe_debug=False, metadata={"kind": "exception"}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -513,7 +513,7 @@ class BaseALwrityAgent(ABC):
event_type="plan", event_type="plan",
severity="info", severity="info",
message=f"{action.action_type} -> {action.target_resource}", message=f"{action.action_type} -> {action.target_resource}",
payload={"action": asdict(action)}, payload=build_agent_event_payload(phase="planning", step="action_received", tool_name=action.action_type, progress_percent=5, input_summary=f"target={action.target_resource}", output_summary="Action accepted for execution", decision_reason="Start run lifecycle", safe_debug=True, metadata={"action": asdict(action)}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -528,7 +528,7 @@ class BaseALwrityAgent(ABC):
event_type="decision", event_type="decision",
severity="warning", severity="warning",
message="Action failed safety validation", message="Action failed safety validation",
payload={"action_id": action.action_id, "action_type": action.action_type}, payload=build_agent_event_payload(phase="validation", step="safety_blocked", tool_name="safety_framework", progress_percent=10, input_summary=action.action_type, output_summary="Action blocked by safety validation", decision_reason="Safety framework rejected action", safe_debug=True, metadata={"action_id": action.action_id, "action_type": action.action_type}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -568,7 +568,7 @@ class BaseALwrityAgent(ABC):
event_type="decision", event_type="decision",
severity="info", severity="info",
message="Action requires approval", message="Action requires approval",
payload={"approval_id": req.id, "action_id": action.action_id}, payload=build_agent_event_payload(phase="approval", step="awaiting_user_decision", tool_name=action.action_type, progress_percent=20, input_summary=action.target_resource, output_summary="Approval request created", decision_reason="Action requires human approval", safe_debug=True, metadata={"approval_id": req.id, "action_id": action.action_id}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -593,7 +593,7 @@ class BaseALwrityAgent(ABC):
event_type="progress", event_type="progress",
severity="info", severity="info",
message="Rollback checkpoint created", message="Rollback checkpoint created",
payload={"checkpoint_id": checkpoint_id}, payload=build_agent_event_payload(phase="safety", step="checkpoint_created", tool_name="rollback_manager", progress_percent=35, output_summary="Rollback checkpoint created", decision_reason="Prepare rollback safety net", safe_debug=True, metadata={"checkpoint_id": checkpoint_id}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -604,7 +604,7 @@ class BaseALwrityAgent(ABC):
event_type="warning", event_type="warning",
severity="warning", severity="warning",
message=str(e)[:2000], message=str(e)[:2000],
payload={"checkpoint": "failed"}, payload=build_agent_event_payload(phase="safety", step="checkpoint_failed", tool_name="rollback_manager", progress_percent=30, output_summary="Checkpoint creation failed", decision_reason="Proceeding without checkpoint", safe_debug=False, metadata={"checkpoint": "failed"}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -639,7 +639,7 @@ class BaseALwrityAgent(ABC):
event_type="final_summary", event_type="final_summary",
severity="info", severity="info",
message=str(result)[:2000] if result is not None else None, message=str(result)[:2000] if result is not None else None,
payload={"action_id": action.action_id}, payload=build_agent_event_payload(phase="execution", step="completed", tool_name=action.action_type, progress_percent=100, output_summary=str(result)[:400] if result is not None else "No output", decision_reason="Action execution completed", safe_debug=True, metadata={"action_id": action.action_id}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )
@@ -690,7 +690,7 @@ class BaseALwrityAgent(ABC):
event_type="error", event_type="error",
severity="error", severity="error",
message=str(e)[:2000], message=str(e)[:2000],
payload={"action_id": action.action_id, "checkpoint_id": checkpoint_id}, payload=build_agent_event_payload(phase="execution", step="failed", tool_name=action.action_type, progress_percent=100, output_summary=str(e)[:400], decision_reason="Exception during action execution", safe_debug=False, metadata={"action_id": action.action_id, "checkpoint_id": checkpoint_id}),
run_id=run_record.id, run_id=run_record.id,
agent_type=self.agent_type, agent_type=self.agent_type,
) )

View File

@@ -14,6 +14,7 @@ from datetime import datetime
from loguru import logger from loguru import logger
from ..txtai_service import TxtaiIntelligenceService from ..txtai_service import TxtaiIntelligenceService
from services.intelligence.agents.core_agent_framework import BaseALwrityAgent, AgentAction, TaskProposal from services.intelligence.agents.core_agent_framework import BaseALwrityAgent, AgentAction, TaskProposal
from services.agent_activity_service import AgentActivityService, build_agent_event_payload
from services.seo_tools.content_strategy_service import ContentStrategyService from services.seo_tools.content_strategy_service import ContentStrategyService
from services.analytics import PlatformAnalyticsService from services.analytics import PlatformAnalyticsService
from services.intelligence.sif_agents import SharedLLMWrapper, LocalLLMWrapper from services.intelligence.sif_agents import SharedLLMWrapper, LocalLLMWrapper
@@ -62,6 +63,41 @@ class SIFBaseAgent(BaseALwrityAgent):
if kwargs: if kwargs:
logger.debug(f"[{self.__class__.__name__}] Parameters: {kwargs}") logger.debug(f"[{self.__class__.__name__}] Parameters: {kwargs}")
db = None
try:
from services.database import get_session_for_user
db = get_session_for_user(self.user_id)
if not db:
return
activity = AgentActivityService(db, self.user_id)
activity.log_event(
event_type="progress",
severity="info",
message=f"{self.__class__.__name__}: {operation}",
payload=build_agent_event_payload(
phase="specialized_agent",
step=operation.lower().replace(" ", "_"),
tool_name=self.__class__.__name__,
input_summary=str(kwargs)[:300] if kwargs else None,
output_summary="Operation invoked",
decision_reason="Agent method execution trace",
safe_debug=True,
metadata={"params": kwargs} if kwargs else {},
),
run_id=None,
agent_type=self.agent_type,
)
except Exception:
pass
finally:
try:
if db:
db.close()
except Exception:
pass
def _create_txtai_agent(self): def _create_txtai_agent(self):
""" """
SIF agents use the intelligence service directly, but we can expose SIF agents use the intelligence service directly, but we can expose

View File

@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
from models.agent_activity_models import AgentAlert from models.agent_activity_models import AgentAlert
from services.agent_activity_service import AgentActivityService from services.agent_activity_service import AgentActivityService, build_agent_event_payload
from services.llm_providers.main_text_generation import llm_text_gen from services.llm_providers.main_text_generation import llm_text_gen
from loguru import logger from loguru import logger
@@ -430,7 +430,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
event_type="plan", event_type="plan",
severity="info", severity="info",
message="Building grounded daily workflow plan", message="Building grounded daily workflow plan",
payload={"grounding": grounding}, payload=build_agent_event_payload(phase="planning", step="build_grounded_plan", tool_name="llm_text_gen", progress_percent=10, input_summary="Grounding data assembled from onboarding + alerts", output_summary="Preparing daily workflow generation", decision_reason="Need context-aware workflow", evidence_refs=["onboarding_data","recent_agent_alerts"], safe_debug=True, metadata={"grounding": grounding}),
run_id=run.id, run_id=run.id,
agent_type="TodayWorkflowGenerator", agent_type="TodayWorkflowGenerator",
) )
@@ -449,7 +449,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
event_type="warning", event_type="warning",
severity="warning", severity="warning",
message=str(e)[:2000], message=str(e)[:2000],
payload={"fallback": True}, payload=build_agent_event_payload(phase="generation", step="llm_failed_fallback", tool_name="llm_text_gen", progress_percent=70, output_summary="LLM generation failed, using fallback tasks", decision_reason="Exception during workflow generation", safe_debug=False, metadata={"fallback": True}),
run_id=run.id, run_id=run.id,
agent_type="TodayWorkflowGenerator", agent_type="TodayWorkflowGenerator",
) )
@@ -467,7 +467,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
event_type="final_summary", event_type="final_summary",
severity="info", severity="info",
message="Daily workflow plan generated", message="Daily workflow plan generated",
payload={"date": date, "task_count": len(result.get("tasks", []))}, payload=build_agent_event_payload(phase="generation", step="workflow_generated", tool_name="llm_text_gen", progress_percent=100, output_summary=f"Generated {len(result.get('tasks', []))} tasks", decision_reason="Workflow assembled successfully", evidence_refs=[date], safe_debug=True, metadata={"date": date, "task_count": len(result.get("tasks", []))}),
run_id=run.id, run_id=run.id,
agent_type="TodayWorkflowGenerator", agent_type="TodayWorkflowGenerator",
) )

View File

@@ -3,202 +3,161 @@ import {
Box, Box,
Paper, Paper,
Typography, Typography,
Avatar,
AvatarGroup,
Chip, Chip,
List, List,
ListItem, ListItem,
ListItemAvatar,
ListItemText,
Divider, Divider,
IconButton, IconButton,
Tooltip Tooltip,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
Stack,
} from '@mui/material'; } from '@mui/material';
import { import {
Psychology as StrategyIcon,
Article as ContentIcon,
Search as SeoIcon,
Campaign as SocialIcon,
CompareArrows as CompetitorIcon,
Refresh as RefreshIcon, Refresh as RefreshIcon,
MoreVert as MoreVertIcon ExpandMore as ExpandMoreIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { apiClient } from '../../../api/client';
interface AgentStatus { type EventPayload = {
id: string; phase?: string | null;
name: string; step?: string | null;
role: string; tool_name?: string | null;
status: 'active' | 'thinking' | 'idle' | 'offline'; progress_percent?: number | null;
current_activity: string; input_summary?: string | null;
icon: React.ElementType; output_summary?: string | null;
color: string; decision_reason?: string | null;
} evidence_refs?: string[] | null;
safe_debug?: boolean;
metadata?: Record<string, unknown>;
};
// Mock data - In real implementation, this would come from a backend endpoint type TeamActivityEvent = {
// /api/agents/status or similar id: number;
const AGENT_TEAM: AgentStatus[] = [ event_type: string;
{ severity: string;
id: 'strategy_architect', message?: string | null;
name: 'Strategy Architect', created_at?: string | null;
role: 'Team Lead', payload?: EventPayload | null;
status: 'active', };
current_activity: 'Analyzing content pillar performance',
icon: StrategyIcon, type AgentRun = {
color: '#6366f1' // Indigo id: number;
}, agent_type: string;
{ status: string;
id: 'content_strategist', started_at?: string | null;
name: 'Content Strategist', };
role: 'Creative',
status: 'thinking',
current_activity: 'Identifying semantic gaps in "AI Tools"',
icon: ContentIcon,
color: '#10b981' // Emerald
},
{
id: 'seo_specialist',
name: 'SEO Specialist',
role: 'Technical',
status: 'idle',
current_activity: 'Monitoring SERP rankings',
icon: SeoIcon,
color: '#f59e0b' // Amber
},
{
id: 'social_manager',
name: 'Social Manager',
role: 'Engagement',
status: 'idle',
current_activity: 'Waiting for new content to schedule',
icon: SocialIcon,
color: '#ec4899' // Pink
},
{
id: 'competitor_analyst',
name: 'Competitor Analyst',
role: 'Intelligence',
status: 'active',
current_activity: 'Scanning competitor X for new posts',
icon: CompetitorIcon,
color: '#ef4444' // Red
}
];
const TeamHuddleWidget: React.FC = () => { 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 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 || [];
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]);
return ( return (
<Paper <Paper elevation={0} sx={{ p: 2, borderRadius: 3, border: '1px solid', borderColor: 'divider', height: '100%' }}>
elevation={0}
sx={{
p: 2,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
height: '100%',
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)'
}}
>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}> <Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6" fontWeight={700} color="text.primary"> <Typography variant="h6" fontWeight={700}>Team Activity</Typography>
Team Huddle <Chip label="Live" size="small" color="success" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
</Typography>
<Chip
label="Live"
size="small"
color="success"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
/>
</Box>
<Box>
<Tooltip title="Refresh Team Status">
<IconButton size="small">
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box> </Box>
<Tooltip title="Refresh Team Activity">
<IconButton size="small" onClick={loadTimeline}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box> </Box>
<List disablePadding> {loading && (
{AGENT_TEAM.map((agent, index) => ( <Box py={4} textAlign="center">
<React.Fragment key={agent.id}> <CircularProgress size={24} />
{index > 0 && <Divider variant="inset" component="li" sx={{ my: 1, ml: 7 }} />} </Box>
<ListItem )}
alignItems="flex-start"
disableGutters {!loading && error && (
sx={{ py: 0.5 }} <Typography variant="body2" color="error">{error}</Typography>
secondaryAction={ )}
<Tooltip title={agent.status}>
<Box {!loading && !error && timeline.length === 0 && (
sx={{ <Typography variant="body2" color="text.secondary">No team activity yet.</Typography>
width: 8, )}
height: 8,
borderRadius: '50%', {!loading && !error && timeline.length > 0 && (
bgcolor: <List disablePadding>
agent.status === 'active' ? '#22c55e' : {timeline.map(({ run, events }, index) => (
agent.status === 'thinking' ? '#3b82f6' : <React.Fragment key={run.id}>
'#94a3b8', {index > 0 && <Divider sx={{ my: 1 }} />}
boxShadow: agent.status === 'active' ? '0 0 0 2px rgba(34, 197, 94, 0.2)' : 'none', <ListItem disableGutters sx={{ display: 'block' }}>
animation: agent.status === 'thinking' ? 'pulse 1.5s infinite' : 'none', <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
'@keyframes pulse': { <Chip size="small" label={run.agent_type || 'Agent'} />
'0%': { opacity: 1, transform: 'scale(1)' }, <Chip size="small" color={run.status === 'completed' ? 'success' : 'warning'} label={run.status} />
'50%': { opacity: 0.6, transform: 'scale(1.2)' }, <Typography variant="caption" color="text.secondary">
'100%': { opacity: 1, transform: 'scale(1)' } {run.started_at ? new Date(run.started_at).toLocaleString() : ''}
}
}}
/>
</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
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> </Typography>
} </Stack>
/>
</ListItem>
</React.Fragment>
))}
</List>
<Box mt={2} pt={2} borderTop="1px solid #eee" display="flex" justifyContent="center"> {events.map((event) => {
<Typography variant="caption" color="primary" sx={{ fontWeight: 600, cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}> const payload = event.payload || {};
View Full Team Activity return (
</Typography> <Accordion key={event.id} disableGutters elevation={0} sx={{ border: '1px solid #e5e7eb', mb: 1 }}>
</Box> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ width: '100%' }}>
<Chip size="small" label={payload.phase || event.event_type} />
{payload.step && <Chip size="small" variant="outlined" label={payload.step} />}
<Typography variant="body2" sx={{ flexGrow: 1 }}>
{event.message || payload.output_summary || 'Activity update'}
</Typography>
{typeof payload.progress_percent === 'number' && (
<Typography variant="caption" color="text.secondary">{payload.progress_percent}%</Typography>
)}
</Stack>
</AccordionSummary>
<AccordionDetails>
<Typography variant="caption" display="block">Tool: {payload.tool_name || '—'}</Typography>
<Typography variant="caption" display="block">Input: {payload.input_summary || '—'}</Typography>
<Typography variant="caption" display="block">Output: {payload.output_summary || '—'}</Typography>
<Typography variant="caption" display="block">Decision: {payload.decision_reason || '—'}</Typography>
<Typography variant="caption" display="block">Evidence: {(payload.evidence_refs || []).join(', ') || '—'}</Typography>
<Typography variant="caption" display="block">Safe debug: {String(payload.safe_debug ?? true)}</Typography>
</AccordionDetails>
</Accordion>
);
})}
</ListItem>
</React.Fragment>
))}
</List>
)}
</Paper> </Paper>
); );
}; };