activity-tab.tsx
1 'use client' 2 3 import { useMemo } from 'react' 4 import { useAppStore } from '@/stores/use-app-store' 5 import { relativeDate, STATUS_STYLES } from '../project-utils' 6 import type { Agent, BoardTask, Schedule } from '@/types' 7 8 const MAX_ITEMS = 50 9 10 export function ActivityTab() { 11 const tasks = useAppStore((s) => s.tasks) as Record<string, BoardTask> 12 const schedules = useAppStore((s) => s.schedules) as Record<string, Schedule> 13 const agents = useAppStore((s) => s.agents) as Record<string, Agent> 14 const activeProjectFilter = useAppStore((s) => s.activeProjectFilter) 15 16 const projectAgents = useMemo( 17 () => Object.values(agents).filter((a) => a.projectId === activeProjectFilter && !a.trashedAt), 18 [agents, activeProjectFilter], 19 ) 20 21 const projectTasks = useMemo( 22 () => Object.values(tasks).filter((t) => t.projectId === activeProjectFilter).sort((a, b) => b.updatedAt - a.updatedAt), 23 [tasks, activeProjectFilter], 24 ) 25 26 const projectSchedules = useMemo( 27 () => Object.values(schedules).filter((s) => s.projectId === activeProjectFilter), 28 [schedules, activeProjectFilter], 29 ) 30 31 const activityItems = useMemo(() => { 32 const items: { id: string; type: 'task' | 'schedule' | 'agent'; name: string; status?: string; time: number }[] = [] 33 for (const t of projectTasks) { 34 items.push({ id: t.id, type: 'task', name: t.title, status: t.status, time: t.updatedAt }) 35 } 36 for (const s of projectSchedules) { 37 if (s.lastRunAt) items.push({ id: s.id, type: 'schedule', name: s.name, status: s.status, time: s.lastRunAt }) 38 } 39 for (const a of projectAgents) { 40 if (a.lastUsedAt) items.push({ id: a.id, type: 'agent', name: a.name, time: a.lastUsedAt }) 41 } 42 return items.sort((a, b) => b.time - a.time) 43 }, [projectTasks, projectSchedules, projectAgents]) 44 45 const displayedItems = activityItems.slice(0, MAX_ITEMS) 46 const truncated = activityItems.length > MAX_ITEMS 47 48 if (activityItems.length === 0) { 49 return ( 50 <div className="max-w-3xl mx-auto px-8 py-6"> 51 <p className="text-[12px] text-text-3/45">No activity yet.</p> 52 </div> 53 ) 54 } 55 56 return ( 57 <div className="max-w-3xl mx-auto px-8 py-6"> 58 <div className="relative pl-5"> 59 <div className="absolute left-[7px] top-2 bottom-2 w-px bg-white/[0.06]" /> 60 <div className="flex flex-col gap-3"> 61 {displayedItems.map((item) => ( 62 <div key={`${item.type}-${item.id}`} className="relative flex items-start gap-3"> 63 <div className={`absolute left-[-13px] top-1.5 w-2 h-2 rounded-full ${ 64 item.type === 'task' && item.status === 'completed' ? 'bg-emerald-400' 65 : item.type === 'task' && item.status === 'running' ? 'bg-sky-400' 66 : item.type === 'task' && item.status === 'failed' ? 'bg-red-400' 67 : item.type === 'schedule' ? 'bg-amber-400' 68 : 'bg-white/[0.2]' 69 }`} /> 70 <div className="flex-1 min-w-0"> 71 <div className="flex items-center gap-2"> 72 <span className="text-[10px] font-600 uppercase tracking-wider text-text-3/40"> 73 {item.type} 74 </span> 75 {item.status && ( 76 <span className={`text-[9px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px] ${STATUS_STYLES[item.status] || 'bg-white/[0.06] text-text-3'}`}> 77 {item.status} 78 </span> 79 )} 80 </div> 81 <p className="text-[12px] text-text-2 truncate mt-0.5">{item.name}</p> 82 </div> 83 <span className="text-[10px] text-text-3/30 shrink-0 mt-0.5">{relativeDate(item.time)}</span> 84 </div> 85 ))} 86 </div> 87 </div> 88 {truncated && ( 89 <p className="text-[11px] text-text-3/40 text-center mt-4">Showing most recent {MAX_ITEMS} items</p> 90 )} 91 </div> 92 ) 93 }