import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { dashboardApi } from "../api/dashboard"; import { activityApi } from "../api/activity"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { MetricCard } from "../components/MetricCard"; import { EmptyState } from "../components/EmptyState"; import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { ActivityRow } from "../components/ActivityRow"; import { Identity } from "../components/Identity"; import { timeAgo } from "../lib/timeAgo"; import { cn, formatCents } from "../lib/utils"; import { Bot, CircleDot, DollarSign, ShieldCheck, LayoutDashboard } from "lucide-react"; import { ActiveAgentsPanel } from "../components/ActiveAgentsPanel"; import type { Agent, Issue } from "@paperclip/shared"; function getRecentIssues(issues: Issue[]): Issue[] { return [...issues] .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); } export function Dashboard() { const { selectedCompanyId, selectedCompany, companies } = useCompany(); const { openOnboarding } = useDialog(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const [animatedActivityIds, setAnimatedActivityIds] = useState>(new Set()); const seenActivityIdsRef = useRef>(new Set()); const hydratedActivityRef = useRef(false); const activityAnimationTimersRef = useRef([]); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); useEffect(() => { setBreadcrumbs([{ label: "Dashboard" }]); }, [setBreadcrumbs]); const { data, isLoading, error } = useQuery({ queryKey: queryKeys.dashboard(selectedCompanyId!), queryFn: () => dashboardApi.summary(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: activity } = useQuery({ queryKey: queryKeys.activity(selectedCompanyId!), queryFn: () => activityApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: issues } = useQuery({ queryKey: queryKeys.issues.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const recentIssues = issues ? getRecentIssues(issues) : []; const recentActivity = useMemo(() => (activity ?? []).slice(0, 10), [activity]); useEffect(() => { for (const timer of activityAnimationTimersRef.current) { window.clearTimeout(timer); } activityAnimationTimersRef.current = []; seenActivityIdsRef.current = new Set(); hydratedActivityRef.current = false; setAnimatedActivityIds(new Set()); }, [selectedCompanyId]); useEffect(() => { if (recentActivity.length === 0) return; const seen = seenActivityIdsRef.current; const currentIds = recentActivity.map((event) => event.id); if (!hydratedActivityRef.current) { for (const id of currentIds) seen.add(id); hydratedActivityRef.current = true; return; } const newIds = currentIds.filter((id) => !seen.has(id)); if (newIds.length === 0) { for (const id of currentIds) seen.add(id); return; } setAnimatedActivityIds((prev) => { const next = new Set(prev); for (const id of newIds) next.add(id); return next; }); for (const id of newIds) seen.add(id); const timer = window.setTimeout(() => { setAnimatedActivityIds((prev) => { const next = new Set(prev); for (const id of newIds) next.delete(id); return next; }); activityAnimationTimersRef.current = activityAnimationTimersRef.current.filter((t) => t !== timer); }, 980); activityAnimationTimersRef.current.push(timer); }, [recentActivity]); useEffect(() => { return () => { for (const timer of activityAnimationTimersRef.current) { window.clearTimeout(timer); } }; }, []); const agentMap = useMemo(() => { const map = new Map(); for (const a of agents ?? []) map.set(a.id, a); return map; }, [agents]); const entityNameMap = useMemo(() => { const map = new Map(); for (const i of issues ?? []) map.set(`issue:${i.id}`, i.title); for (const a of agents ?? []) map.set(`agent:${a.id}`, a.name); for (const p of projects ?? []) map.set(`project:${p.id}`, p.name); return map; }, [issues, agents, projects]); const agentName = (id: string | null) => { if (!id || !agents) return null; return agents.find((a) => a.id === id)?.name ?? null; }; if (!selectedCompanyId) { if (companies.length === 0) { return ( ); } return ( ); } return (
{selectedCompany && (

{selectedCompany.name}

)} {isLoading &&

Loading...

} {error &&

{error.message}

} {data && ( <>
navigate("/agents")} description={ navigate("/agents")}>{data.agents.running} running {", "} navigate("/agents")}>{data.agents.paused} paused {", "} navigate("/agents")}>{data.agents.error} errors } /> navigate("/issues")} description={ navigate("/issues")}>{data.tasks.open} open {", "} navigate("/issues")}>{data.tasks.blocked} blocked } /> navigate("/costs")} description={ navigate("/costs")}> {data.costs.monthBudgetCents > 0 ? `${data.costs.monthUtilizationPercent}% of ${formatCents(data.costs.monthBudgetCents)} budget` : "Unlimited budget"} } /> navigate("/approvals")} description={ navigate("/issues")}> {data.staleTasks} stale tasks } />
{/* Recent Activity */} {recentActivity.length > 0 && (

Recent Activity

{recentActivity.map((event) => ( ))}
)} {/* Recent Tasks */}

Recent Tasks

{recentIssues.length === 0 ? (

No tasks yet.

) : (
{recentIssues.slice(0, 10).map((issue) => (
navigate(`/issues/${issue.id}`)} >

{issue.title} {issue.assigneeAgentId && (() => { const name = agentName(issue.assigneeAgentId); return name ? : null; })()}

{timeAgo(issue.updatedAt)}
))}
)}
)}
); }