/ src / components / projects / tabs / activity-tab.tsx
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  }