'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Loader } from '@/components/ui/loader' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' interface Activity { id: number type: string entity_type: string entity_id: number actor: string description: string data?: any created_at: number entity?: { type: string id?: number title?: string name?: string status?: string content_preview?: string task_title?: string } } interface SessionInfo { id: string key: string kind: string age: string model: string tokens: string active: boolean } const activityIcons: Record = { task_created: '+', task_updated: '~', task_deleted: 'x', comment_added: '#', agent_created: '@', agent_status_change: '~', standup_generated: '!', mention: '>', assignment: '=', } const activityColors: Record = { task_created: 'text-green-400', task_updated: 'text-blue-400', task_deleted: 'text-red-400', comment_added: 'text-purple-400', agent_created: 'text-cyan-400', agent_status_change: 'text-yellow-400', standup_generated: 'text-orange-400', mention: 'text-pink-400', assignment: 'text-indigo-400', } function formatRelativeTime(timestamp: number) { const now = Date.now() const diffMs = now - timestamp * 1000 const diffMinutes = Math.floor(diffMs / (1000 * 60)) const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) if (diffMinutes < 1) return 'Just now' if (diffMinutes < 60) return `${diffMinutes}m ago` if (diffHours < 24) return `${diffHours}h ago` if (diffDays < 7) return `${diffDays}d ago` return new Date(timestamp * 1000).toLocaleDateString() } function groupByDay(activities: Activity[]): Record { const groups: Record = {} for (const act of activities) { const day = new Date(act.created_at * 1000).toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric', }) if (!groups[day]) groups[day] = [] groups[day].push(act) } return groups } // ── Activity row (flat feed) ──────────────────── function ActivityRow({ activity }: { activity: Activity }) { const t = useTranslations('activityFeed') return (
{activityIcons[activity.type] || '•'}

{activity.actor}{' '} {activity.description}

{activity.entity && (
{activity.entity.type === 'task' && (
{t('entityTask')} {activity.entity.title} {activity.entity.status && ( {activity.entity.status} )}
)} {activity.entity.type === 'comment' && (
{t('entityCommentOn')} {activity.entity.task_title} {activity.entity.content_preview && (
"{activity.entity.content_preview}..."
)}
)} {activity.entity.type === 'agent' && (
{t('entityAgent')} {activity.entity.name} {activity.entity.status && ( {activity.entity.status} )}
)}
)} {activity.data && Object.keys(activity.data).length > 0 && (
{t('showDetails')}
                    {JSON.stringify(activity.data, null, 2)}
                  
)}
{formatRelativeTime(activity.created_at)}
) } // ── Timeline row (agent-grouped view) ─────────── function TimelineRow({ activity }: { activity: Activity }) { return (
{activityIcons[activity.type] || '?'}

{activity.description}

{activity.entity?.title && (

{activity.entity.type === 'task' ? `${activity.entity.title}` : activity.entity.title}

)}
{new Date(activity.created_at * 1000).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', })}
) } // ── Main Component ────────────────────────────── export function ActivityFeedPanel() { const t = useTranslations('activityFeed') const { agents } = useMissionControl() const [activities, setActivities] = useState([]) const [sessions, setSessions] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [autoRefresh, setAutoRefresh] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(0) const [lastRefresh, setLastRefresh] = useState(Date.now()) const [selectedAgent, setSelectedAgent] = useState('') const [filter, setFilter] = useState({ type: '', limit: 50 }) const limit = filter.limit const isAgentView = selectedAgent !== '' // ── Fetch activities ────────────────────────── const fetchActivities = useCallback( async (since?: number) => { try { if (!since) setLoading(true) setError(null) const params = new URLSearchParams() if (selectedAgent) params.append('actor', selectedAgent) if (filter.type) params.append('type', filter.type) params.append('limit', limit.toString()) if (isAgentView) params.append('offset', (page * limit).toString()) if (since && !isAgentView) params.append('since', Math.floor(since / 1000).toString()) const response = await fetch(`/api/activities?${params}`) if (!response.ok) throw new Error('Failed to fetch activities') const data = await response.json() if (since && !isAgentView) { setActivities((prev) => { const newActivities = data.activities || [] const existingIds = new Set(prev.map((a: Activity) => a.id)) const uniqueNew = newActivities.filter((a: Activity) => !existingIds.has(a.id)) return [...uniqueNew, ...prev].slice(0, limit) }) } else { setActivities(data.activities || []) } setTotal(data.total || 0) setLastRefresh(Date.now()) } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred') } finally { setLoading(false) } }, [selectedAgent, filter.type, limit, page, isAgentView], ) const lastRefreshRef = useRef(lastRefresh) useEffect(() => { lastRefreshRef.current = lastRefresh }, [lastRefresh]) useEffect(() => { fetchActivities() }, [fetchActivities]) const pollActivities = useCallback(() => { fetchActivities(isAgentView ? undefined : lastRefreshRef.current) }, [fetchActivities, isAgentView]) useSmartPoll(pollActivities, 30000, { enabled: autoRefresh, pauseWhenSseConnected: true }) // ── Fetch sessions (for agent sidebar) ──────── const fetchSessions = useCallback(async () => { try { const res = await fetch('/api/sessions') if (!res.ok) return const data = await res.json() setSessions(data.sessions || []) } catch { /* silent */ } }, []) useEffect(() => { fetchSessions() }, [fetchSessions]) // ── Derived data ────────────────────────────── const activityTypes = Array.from(new Set(activities.map((a) => a.type))).sort() const agentSessions = sessions.filter((s) => selectedAgent && s.key.includes(selectedAgent)) const selectedAgentData = agents.find((a) => a.name === selectedAgent) const totalPages = Math.ceil(total / limit) const groupedByDay = isAgentView ? groupByDay(activities) : {} return (
{/* Header */}

{t('title')}

{/* Filters + Agent Selector */}
{/* Agent filter */}
{agents.map((a) => ( ))}
{/* Type filter */}
{/* Limit */}
{/* Error */} {error && (
{error}
)} {/* Content */}
{loading && activities.length === 0 ? (
) : activities.length === 0 ? (

{t('noActivities')}

{selectedAgent ? t('noActivityForAgent', { agent: selectedAgent }) : t('tryAdjustingFilters')}

) : isAgentView ? ( /* ── Agent-grouped view with sidebar ─── */
{/* Agent info sidebar */}
{selectedAgentData && (
{selectedAgentData.name.slice(0, 2).toUpperCase()}

{selectedAgentData.name}

{selectedAgentData.role}

{t('agentStatus')} {selectedAgentData.status}
{selectedAgentData.last_seen && (
{t('lastSeen')} {formatRelativeTime(selectedAgentData.last_seen)}
)} {selectedAgentData.last_activity && (
{t('lastAction')} {selectedAgentData.last_activity}
)} {selectedAgentData.taskStats && ( <>
{t('tasksAssigned')} {selectedAgentData.taskStats.assigned}
{t('inProgress')} {selectedAgentData.taskStats.in_progress}
{t('completed')} {selectedAgentData.taskStats.completed}
)}
)} {agentSessions.length > 0 && (

{t('activeSessions')}

{agentSessions.map((s) => (
{s.kind}
{s.model} {s.tokens} tokens {s.age}
))}
)}
{/* Day-grouped timeline */}
{Object.entries(groupedByDay).map(([day, dayActivities]) => (
{day} {t('events', { count: dayActivities.length })}
{dayActivities.map((act) => ( ))}
))} {totalPages > 1 && (
{t('pageOf', { page: page + 1, total: totalPages })}
)}
) : ( /* ── Flat feed (all agents) ──────── */
{activities.map((activity, index) => ( ))}
)}
{/* Footer */}
{isAgentView ? t('footerAgentEvents', { total, agent: selectedAgent }) : t('footerShowing', { count: activities.length, filtered: filter.type ? ` ${t('filtered')}` : '' })} {t('lastUpdated', { time: new Date(lastRefresh).toLocaleTimeString() })}
) }