/** * EventList Component * * Displays a list of project events with: * - Event type icons and styling * - Relative timestamps * - Actor information * - Expandable payload details */ 'use client'; import { useState } from 'react'; import { formatDistanceToNow } from 'date-fns'; import { Bot, FileText, Users, CheckCircle2, XCircle, PlayCircle, AlertTriangle, Folder, Workflow, ChevronDown, ChevronRight, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { EventType, type ProjectEvent, isAgentEvent, isIssueEvent, isSprintEvent, isApprovalEvent, isWorkflowEvent, isProjectEvent, } from '@/lib/types/events'; // ============================================================================ // Types // ============================================================================ interface EventListProps { /** Events to display */ events: ProjectEvent[]; /** Maximum height for scrolling (default: 400px) */ maxHeight?: number | string; /** Whether to show the header (default: true) */ showHeader?: boolean; /** Title for the header (default: 'Activity Feed') */ title?: string; /** Whether to show event payloads (default: false) */ showPayloads?: boolean; /** Empty state message */ emptyMessage?: string; /** Additional CSS classes */ className?: string; /** Callback when event is clicked */ onEventClick?: (event: ProjectEvent) => void; } interface EventItemProps { /** Event to display */ event: ProjectEvent; /** Whether to show payload details */ showPayload?: boolean; /** Callback when clicked */ onClick?: (event: ProjectEvent) => void; } // ============================================================================ // Helper Functions // ============================================================================ function getEventConfig(event: ProjectEvent) { if (isAgentEvent(event)) { switch (event.type) { case EventType.AGENT_SPAWNED: return { icon: Bot, label: 'Agent Spawned', color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900', }; case EventType.AGENT_MESSAGE: return { icon: Bot, label: 'Agent Message', color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900', }; case EventType.AGENT_STATUS_CHANGED: return { icon: Bot, label: 'Status Changed', color: 'text-yellow-500', bgColor: 'bg-yellow-100 dark:bg-yellow-900', }; case EventType.AGENT_TERMINATED: return { icon: Bot, label: 'Agent Terminated', color: 'text-gray-500', bgColor: 'bg-gray-100 dark:bg-gray-800', }; } } if (isIssueEvent(event)) { switch (event.type) { case EventType.ISSUE_CREATED: return { icon: FileText, label: 'Issue Created', color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900', }; case EventType.ISSUE_UPDATED: return { icon: FileText, label: 'Issue Updated', color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900', }; case EventType.ISSUE_ASSIGNED: return { icon: Users, label: 'Issue Assigned', color: 'text-purple-500', bgColor: 'bg-purple-100 dark:bg-purple-900', }; case EventType.ISSUE_CLOSED: return { icon: CheckCircle2, label: 'Issue Closed', color: 'text-green-600', bgColor: 'bg-green-100 dark:bg-green-900', }; } } if (isSprintEvent(event)) { return { icon: PlayCircle, label: event.type === EventType.SPRINT_STARTED ? 'Sprint Started' : 'Sprint Completed', color: 'text-indigo-500', bgColor: 'bg-indigo-100 dark:bg-indigo-900', }; } if (isApprovalEvent(event)) { switch (event.type) { case EventType.APPROVAL_REQUESTED: return { icon: AlertTriangle, label: 'Approval Requested', color: 'text-orange-500', bgColor: 'bg-orange-100 dark:bg-orange-900', }; case EventType.APPROVAL_GRANTED: return { icon: CheckCircle2, label: 'Approval Granted', color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900', }; case EventType.APPROVAL_DENIED: return { icon: XCircle, label: 'Approval Denied', color: 'text-red-500', bgColor: 'bg-red-100 dark:bg-red-900', }; } } if (isProjectEvent(event)) { return { icon: Folder, label: event.type.replace('project.', '').replace('_', ' '), color: 'text-teal-500', bgColor: 'bg-teal-100 dark:bg-teal-900', }; } if (isWorkflowEvent(event)) { switch (event.type) { case EventType.WORKFLOW_STARTED: return { icon: Workflow, label: 'Workflow Started', color: 'text-cyan-500', bgColor: 'bg-cyan-100 dark:bg-cyan-900', }; case EventType.WORKFLOW_STEP_COMPLETED: return { icon: Workflow, label: 'Step Completed', color: 'text-cyan-500', bgColor: 'bg-cyan-100 dark:bg-cyan-900', }; case EventType.WORKFLOW_COMPLETED: return { icon: CheckCircle2, label: 'Workflow Completed', color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900', }; case EventType.WORKFLOW_FAILED: return { icon: XCircle, label: 'Workflow Failed', color: 'text-red-500', bgColor: 'bg-red-100 dark:bg-red-900', }; } } // Default fallback return { icon: FileText, label: event.type, color: 'text-gray-500', bgColor: 'bg-gray-100 dark:bg-gray-800', }; } function getEventSummary(event: ProjectEvent): string { const payload = event.payload as Record; switch (event.type) { case EventType.AGENT_SPAWNED: return `${payload.agent_name || 'Agent'} spawned as ${payload.role || 'unknown role'}`; case EventType.AGENT_MESSAGE: return String(payload.message || 'No message'); case EventType.AGENT_STATUS_CHANGED: return `Status: ${payload.previous_status} -> ${payload.new_status}`; case EventType.AGENT_TERMINATED: return payload.termination_reason ? String(payload.termination_reason) : 'Agent terminated'; case EventType.ISSUE_CREATED: return String(payload.title || 'New issue created'); case EventType.ISSUE_UPDATED: return `Issue ${payload.issue_id || ''} updated`; case EventType.ISSUE_ASSIGNED: return payload.assignee_name ? `Assigned to ${payload.assignee_name}` : 'Issue assignment changed'; case EventType.ISSUE_CLOSED: return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed'; case EventType.SPRINT_STARTED: return payload.sprint_name ? `Sprint "${payload.sprint_name}" started` : 'Sprint started'; case EventType.SPRINT_COMPLETED: return payload.sprint_name ? `Sprint "${payload.sprint_name}" completed` : 'Sprint completed'; case EventType.APPROVAL_REQUESTED: return String(payload.description || 'Approval requested'); case EventType.APPROVAL_GRANTED: return 'Approval granted'; case EventType.APPROVAL_DENIED: return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied'; case EventType.WORKFLOW_STARTED: return payload.workflow_type ? `${payload.workflow_type} workflow started` : 'Workflow started'; case EventType.WORKFLOW_STEP_COMPLETED: return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`; case EventType.WORKFLOW_COMPLETED: return payload.duration_seconds ? `Completed in ${payload.duration_seconds}s` : 'Workflow completed'; case EventType.WORKFLOW_FAILED: return payload.error_message ? String(payload.error_message) : 'Workflow failed'; default: return event.type; } } function formatActorDisplay(event: ProjectEvent): string { if (event.actor_type === 'system') return 'System'; if (event.actor_type === 'agent') return 'Agent'; if (event.actor_type === 'user') return 'User'; return event.actor_type; } // ============================================================================ // EventItem Component // ============================================================================ function EventItem({ event, showPayload = false, onClick }: EventItemProps) { const [isExpanded, setIsExpanded] = useState(false); const config = getEventConfig(event); const Icon = config.icon; const summary = getEventSummary(event); const actor = formatActorDisplay(event); const timestamp = formatDistanceToNow(new Date(event.timestamp), { addSuffix: true }); const handleClick = () => { if (showPayload) { setIsExpanded(!isExpanded); } onClick?.(event); }; return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } } : undefined } > {/* Icon */}
{/* Content */}
{config.label} {actor}

{summary}

{timestamp} {showPayload && ( )}
{/* Expanded payload */} {showPayload && isExpanded && (
              {JSON.stringify(event.payload, null, 2)}
            
)}
); } // ============================================================================ // EventList Component // ============================================================================ /** * EventList - Display project activity feed * * Features: * - Event type icons and colors * - Event summaries * - Relative timestamps * - Actor display * - Expandable payload details * - Scrollable container * - Empty state handling * * @example * ```tsx * console.log('Clicked:', e)} * /> * ``` */ export function EventList({ events, maxHeight = 400, showHeader = true, title = 'Activity Feed', showPayloads = false, emptyMessage = 'No events yet', className, onEventClick, }: EventListProps) { // Sort events by timestamp, newest first const sortedEvents = [...events].sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); return ( {showHeader && ( {title} {events.length > 0 && ( {events.length} event{events.length !== 1 ? 's' : ''} )} )} {sortedEvents.length === 0 ? (

{emptyMessage}

) : (
{sortedEvents.map((event) => ( ))}
)}
); }