import { useCallback, useMemo, useState } from 'react'; import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Clock, Filter, Info, Pause, Play, RefreshCw, RotateCcw, Search, Zap, } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import type { ChatMessage, ChatActivityItem } from '@/app-types'; type ActivityPageProps = { chatMessages: ChatMessage[]; coworkMessages: ChatMessage[]; activeSessionKey: string; coworkSessionKey: string; gatewayConnected: boolean; }; type ActivityEntry = { id: string; timestamp: number; source: 'chat' | 'cowork'; sessionKey: string; role: 'user' | 'assistant' | 'system'; text: string; activities: ChatActivityItem[]; }; type ToneFilter = 'all' | 'success' | 'danger' | 'neutral'; function extractActivities(msg: ChatMessage): ChatActivityItem[] { if (msg.meta?.kind === 'activity' && Array.isArray(msg.meta.items)) { return msg.meta.items; } return []; } function timeAgo(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'Just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)} hr ago`; const days = Math.floor(seconds / 86400); return `${days} day${days === 1 ? '' : 's'} ago`; } const TONE_CONFIG = { success: { icon: CheckCircle2, color: 'text-emerald-600', bg: 'bg-emerald-50 dark:bg-emerald-950/30', border: 'border-emerald-200 dark:border-emerald-800/50', label: 'Success' }, danger: { icon: AlertTriangle, color: 'text-red-600', bg: 'bg-red-50 dark:bg-red-950/30', border: 'border-red-200 dark:border-red-800/50', label: 'Error' }, neutral: { icon: Info, color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-950/30', border: 'border-blue-200 dark:border-blue-800/50', label: 'Info' }, } as const; export function ActivityPage({ chatMessages, coworkMessages, activeSessionKey, coworkSessionKey, gatewayConnected, }: ActivityPageProps) { const [filterQuery, setFilterQuery] = useState(''); const [toneFilter, setToneFilter] = useState('all'); const [sourceFilter, setSourceFilter] = useState<'all' | 'chat' | 'cowork'>('all'); const [expandedEntries, setExpandedEntries] = useState>(new Set()); const entries = useMemo(() => { const all: ActivityEntry[] = []; let counter = 0; const processMessages = (messages: ChatMessage[], source: 'chat' | 'cowork', sessionKey: string) => { for (const msg of messages) { counter++; const activities = extractActivities(msg); all.push({ id: `${source}-${counter}`, timestamp: Date.now() - (messages.length - counter) * 60_000, source, sessionKey, role: msg.role, text: msg.text, activities, }); } }; processMessages(chatMessages, 'chat', activeSessionKey); counter = 0; processMessages(coworkMessages, 'cowork', coworkSessionKey); return all.sort((a, b) => b.timestamp - a.timestamp); }, [chatMessages, coworkMessages, activeSessionKey, coworkSessionKey]); const filteredEntries = useMemo(() => { let result = entries; if (sourceFilter !== 'all') { result = result.filter((e) => e.source === sourceFilter); } if (toneFilter !== 'all') { result = result.filter((e) => e.activities.some((a) => a.tone === toneFilter)); } if (filterQuery.trim()) { const q = filterQuery.toLowerCase(); result = result.filter( (e) => e.text.toLowerCase().includes(q) || e.activities.some((a) => a.label.toLowerCase().includes(q) || a.details?.toLowerCase().includes(q)), ); } return result; }, [entries, sourceFilter, toneFilter, filterQuery]); const stats = useMemo(() => { const total = entries.length; const withActivity = entries.filter((e) => e.activities.length > 0).length; const successes = entries.filter((e) => e.activities.some((a) => a.tone === 'success')).length; const errors = entries.filter((e) => e.activities.some((a) => a.tone === 'danger')).length; return { total, withActivity, successes, errors }; }, [entries]); const toggleExpand = useCallback((id: string) => { setExpandedEntries((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); return (
{/* Header */}

Activity

{gatewayConnected ? 'Live' : 'Offline'}

Real-time overview of all agent actions, tool calls, and system events.

{/* Stats bar */}
{stats.total} events
{stats.successes} success
{stats.errors} errors
{stats.withActivity} with tool calls
{/* Filter bar + timeline */}
{/* Filters */}
setFilterQuery(e.target.value)} placeholder="Search activity..." className="h-7 border-0 bg-transparent px-0 text-[12px] shadow-none focus-visible:ring-0" />
{(['all', 'chat', 'cowork'] as const).map((src) => ( ))}
{(['all', 'success', 'danger', 'neutral'] as const).map((tone) => ( ))}
{/* Timeline */} {filteredEntries.length === 0 ? (

{filterQuery || toneFilter !== 'all' || sourceFilter !== 'all' ? 'No matches for this filter.' : 'No activity yet. Start a chat or a cowork task.'}

) : (
{/* Vertical timeline line */}
{filteredEntries.map((entry) => { const hasActivities = entry.activities.length > 0; const isExpanded = expandedEntries.has(entry.id); const primaryTone = entry.activities.find((a) => a.tone === 'danger')?.tone || entry.activities.find((a) => a.tone === 'success')?.tone || 'neutral'; const ToneIcon = entry.role === 'user' ? Play : (hasActivities ? TONE_CONFIG[primaryTone].icon : Clock); return (
{/* Timeline dot */}
{/* Content */}
{entry.source} {entry.role === 'user' ? 'You' : 'Agent'} {timeAgo(entry.timestamp)}

{entry.text.slice(0, 200)} {entry.text.length > 200 ? '…' : ''}

{/* Activity items */} {hasActivities && (
{isExpanded && (
{entry.activities.map((activity) => { const aConf = TONE_CONFIG[activity.tone]; const AIcon = aConf.icon; return (

{activity.label}

{activity.details && (

{activity.details}

)}
); })}
)}
)}
); })}
)}
); }